From 291666026cf9c961231f5dfb873966e0b5ec5ea1 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 2 Jan 2024 23:31:28 -0800 Subject: [PATCH] Working UI Branch (#919) * update deploy workflow * update deploy workflow conditions * update homepage url to match pages deployment * update deploy workflow to export proper directory * update img urls * complete portfolio section(queries) * add prod and dev env's for queries * - make prod ready - ibc transactions * add https endpoints for prod * add qToken deposit & withdraw modals * Assets Page WIP * DEFI Page WIP * Begin Airdrop page * Update Airdrop page * Add more testnet endpoints * - Add osmosis testnet, - Add validaotr logos to stkaing box * add missed blocks counter query * remove missed blocks from validator table * rough dev mode * add else if switcher for APR chain-id in staking box * rename dev deploy file | prep for prod deploy file * fix dev deploy file environment * semi working testnet tx's * begin test net addition to intent query * recent changes * fix memo buffer * fix all query errors, fix val logo errors, fix font errors * add defi query, swap index page for staking, fix bad balance query * fix dom nesting errors * finish defi box * readme updates, add validator name(s) to staking modal confirm section * re add interchain css for modal popup * begin custom wallet modal theme * install chain registry fork, update some endpoints, add more wallet modal customization * add better error/success handling for unstake tx, fix step handling in stakingProcessModal * fix wallet error, add empty export to bypass error for staking url page * add filler to allow build * add more mobile resisizng * update todo, misc ui updates * make unbonding table header sticky --------- Co-authored-by: Jacob Gadikian Co-authored-by: Joe Bowman --- ...{testuideploy.yaml => devWebUiDeploy.yaml} | 23 +- web-ui/.env | 21 +- web-ui/.env.development | 29 ++ web-ui/README.md | 42 +-- web-ui/bun.lockb | Bin 419315 -> 432937 bytes web-ui/components/Airdrop/airdropSection.tsx | 111 ++++++++ web-ui/components/Assets/assetsGrid.tsx | 187 ++++++------- web-ui/components/Assets/index.ts | 6 + web-ui/components/Assets/intents.tsx | 55 +++- web-ui/components/Assets/modals/index.ts | 4 + .../Assets/modals/qTokenDepositModal.tsx | 224 ++++++++++++++++ .../Assets/modals/qTokenWithdrawlModal.tsx | 224 ++++++++++++++++ .../Assets/modals/qckDepositModa.tsx | 144 ---------- .../Assets/modals/qckDepositModal.tsx | 215 +++++++++++++++ .../Assets/modals/qckWithdrawModal.tsx | 123 +++++++-- web-ui/components/Assets/portfolio.tsx | 32 ++- web-ui/components/Assets/quickbox.tsx | 22 +- web-ui/components/Assets/unbondingTable.tsx | 118 ++++++--- web-ui/components/Defi/defiBox.tsx | 212 +++++++++++++++ web-ui/components/Staking/assetsAccordion.tsx | 37 ++- web-ui/components/Staking/infoBox.tsx | 97 ++++--- .../Staking/modals/stakingProcessModal.tsx | 97 ++++++- .../modals/validatorSelectionModal.tsx | 18 +- .../Staking/modals/validatorTable.tsx | 160 +++++++---- .../Staking/networkSelectButton.tsx | 4 +- web-ui/components/Staking/stakingBox.tsx | 119 +++++---- .../components/ThreeJS/liquidMetalSphere.tsx | 115 +++++--- web-ui/components/react/chain-dropdown.tsx | 54 +++- web-ui/components/react/sideHeader.tsx | 72 +++-- web-ui/hooks/index.ts | 2 +- web-ui/hooks/useGrpcQueryClient.ts | 35 +-- web-ui/hooks/useQueries.ts | 184 ++++++++++++- web-ui/hooks/useQueryHooks2.ts | 9 +- web-ui/hooks/useRpcQueryClient.ts | 23 +- web-ui/hooks/useToaster.tsx | 52 ++-- web-ui/hooks/{useTx.ts => useTx.tsx} | 33 ++- web-ui/hooks/useValidatorData.ts | 75 ------ web-ui/next.config.js | 13 +- web-ui/package.json | 8 +- web-ui/pages/_app.tsx | 205 +++++++++++++- web-ui/pages/airdrop.tsx | 34 +++ web-ui/pages/assets.tsx | 250 ++++++++++++------ web-ui/pages/defi.tsx | 33 ++- web-ui/pages/governance.tsx | 37 +-- web-ui/pages/index.tsx | 24 -- .../staking/[chainId]/[valoperAddress].tsx | 9 + .../pages/{staking.tsx => staking/index.tsx} | 28 +- web-ui/public/img/bgLogo.svg | 21 ++ web-ui/public/img/networks/qregen.png | Bin 0 -> 28660 bytes web-ui/public/img/textLogo.svg | 103 ++++++++ web-ui/services/zone.ts | 136 +--------- web-ui/state/chains/prod.ts | 64 ++++- web-ui/test/Staking.test.tsx | 20 +- web-ui/tx/ibcTransferTx.tsx | 108 ++++++++ web-ui/tx/liquidStakeTx.tsx | 7 +- web-ui/utils/ibc.ts | 81 ++++++ web-ui/utils/index.ts | 1 + web-ui/utils/logos.ts | 48 ++-- web-ui/utils/staking.ts | 60 ++++- 59 files changed, 3214 insertions(+), 1054 deletions(-) rename .github/workflows/{testuideploy.yaml => devWebUiDeploy.yaml} (65%) create mode 100644 web-ui/.env.development create mode 100644 web-ui/components/Airdrop/airdropSection.tsx create mode 100644 web-ui/components/Assets/index.ts create mode 100644 web-ui/components/Assets/modals/index.ts create mode 100644 web-ui/components/Assets/modals/qTokenDepositModal.tsx create mode 100644 web-ui/components/Assets/modals/qTokenWithdrawlModal.tsx delete mode 100644 web-ui/components/Assets/modals/qckDepositModa.tsx create mode 100644 web-ui/components/Assets/modals/qckDepositModal.tsx create mode 100644 web-ui/components/Defi/defiBox.tsx rename web-ui/hooks/{useTx.ts => useTx.tsx} (70%) delete mode 100644 web-ui/hooks/useValidatorData.ts create mode 100644 web-ui/pages/airdrop.tsx delete mode 100644 web-ui/pages/index.tsx create mode 100644 web-ui/pages/staking/[chainId]/[valoperAddress].tsx rename web-ui/pages/{staking.tsx => staking/index.tsx} (80%) create mode 100644 web-ui/public/img/bgLogo.svg create mode 100644 web-ui/public/img/networks/qregen.png create mode 100644 web-ui/public/img/textLogo.svg create mode 100644 web-ui/tx/ibcTransferTx.tsx create mode 100644 web-ui/utils/ibc.ts diff --git a/.github/workflows/testuideploy.yaml b/.github/workflows/devWebUiDeploy.yaml similarity index 65% rename from .github/workflows/testuideploy.yaml rename to .github/workflows/devWebUiDeploy.yaml index a5b305d62..20c46e83e 100644 --- a/.github/workflows/testuideploy.yaml +++ b/.github/workflows/devWebUiDeploy.yaml @@ -1,16 +1,26 @@ -name: Test Deploy to GitHub Pages +name: Build & Deploy web-ui with dev environment to Github Pages on: push: + branches: + - bunDev + paths: + - web-ui/** + pull_request: + branches: + - bunDev paths: - web-ui/** workflow_dispatch: permissions: - contents: read + contents: write pages: write id-token: write + actions: write + deployments: write + pull-requests: write concurrency: group: "pages" @@ -26,7 +36,12 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: latest # or specify a version + bun-version: latest + + - name: Setup Environment File + run: | + cd web-ui + cp .env.development .env - name: Install Dependencies run: | @@ -47,4 +62,4 @@ jobs: uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./out + publish_dir: ./web-ui/out diff --git a/web-ui/.env b/web-ui/.env index ec7efd392..b4428c9da 100644 --- a/web-ui/.env +++ b/web-ui/.env @@ -1,4 +1,16 @@ -NEXT_PUBLIC_CHAIN_ENV="preprod" +NEXT_PUBLIC_CHAIN_ENV="mainnet" +NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_QUICKSILVER="https://lcd.quicksilver.zone/" +NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_QUICKSILVER="https://rpc.quicksilver.zone/" +NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_COSMOSHUB=https://lcd.cosmoshub-4.quicksilver.zone +NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_COSMOSHUB=https://rpc.cosmoshub-4.quicksilver.zone +NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_OSMOSIS="https://lcd.osmosis-1.quicksilver.zone" +NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_OSMOSIS="https://rpc.osmosis-1.quicksilver.zone" +NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_STARGAZE="https://lcd.stargaze-1.quicksilver.zone" +NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_STARGAZE="https://rpc.stargaze-1.quicksilver.zone" +NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_REGEN="https://lcd.regen-1.quicksilver.zone" +NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_REGEN="https://rpc.regen-1.quicksilver.zone" +NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_SOMMELIER="https://lcd.sommelier-3.quicksilver.zone" +NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_SOMMELIER="https://rpc.sommelier-3.quicksilver.zone" NEXT_PUBLIC_QUICKSILVER_API="https://lcd.quicksilver.zone" NEXT_PUBLIC_QUICKSILVER_DATA_API="https://data.quicksilver.zone" ZONE_URL="quicksilver.zone" @@ -9,4 +21,9 @@ REACT_APP_ENABLE_CLAIMS="true" 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" \ No newline at end of file +NEXT_PUBLIC_WHITELISTED_ZONES="osmosis-1,stargaze-1,regen-1,cosmoshub-4,sommelier-3" +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 \ No newline at end of file diff --git a/web-ui/.env.development b/web-ui/.env.development new file mode 100644 index 000000000..75d989365 --- /dev/null +++ b/web-ui/.env.development @@ -0,0 +1,29 @@ +NEXT_PUBLIC_CHAIN_ENV="testnet" +NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_QUICKSILVER="https://lcd.test.quicksilver.zone" +NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_QUICKSILVER="https://rpc.test.quicksilver.zone" +NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_COSMOSHUB=https://lcd.provider.test.quicksilver.zone +NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_COSMOSHUB=https://rpc.provider.test.quicksilver.zone +NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_OSMOSIS="https://lcd.osmo-test-5.test.quicksilver.zone" +NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_OSMOSIS="https://rpc.osmo-test-5.test.quicksilver.zone" +NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_STARGAZE="https://lcd.elgafar-1.test.quicksilver.zone" +NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_STARGAZE="https://rpc.elgafar-1.test.quicksilver.zone" +NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_REGEN="https://lcd.regen-redwood-1.test.quicksilver.zone" +NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_REGEN="https://rpc.regen-redwood-1.test.quicksilver.zone" +NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_SOMMELIER="https://lcd.sommelier-3.quicksilver.zone" +NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_SOMMELIER="https://rpc.sommelier-3.quicksilver.zone" +NEXT_PUBLIC_QUICKSILVER_API="https://lcd.test.quicksilver.zone" +NEXT_PUBLIC_QUICKSILVER_DATA_API="https://data.test.quicksilver.zone" +ZONE_URL="quicksilver.zone" +REACT_APP_WHITELISTED_ZONES="osmosis-1,stargaze-1,regen-1,cosmoshub-4,sommelier-3" +REACT_APP_ENABLE_UNBONDING="true" +REACT_APP_ENABLE_SET_INTENT="true" +REACT_APP_ENABLE_CLAIMS="true" +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_COSMOSHUB_CHAIN_ID=provider +NEXT_PUBLIC_OSMOSIS_CHAIN_ID=osmo-test-5 +NEXT_PUBLIC_STARGAZE_CHAIN_ID=elgafar-1 +NEXT_PUBLIC_REGEN_CHAIN_ID=regen-redwood-1 +NEXT_PUBLIC_SOMMELIER_CHAIN_ID=sommelier-3 \ No newline at end of file diff --git a/web-ui/README.md b/web-ui/README.md index e5c9285b1..1b5c9e277 100644 --- a/web-ui/README.md +++ b/web-ui/README.md @@ -33,32 +33,44 @@ Please ensure your IDE is configured to use Typescript v4.9.3 ### Development ToDo -**Staking Page** - -- figure out a better way to fit the custom weight Ui into the modal. +**Governance** -- need to handle number displays better, sometimes they show NaN or undefined but quickly render - -- need to update any QS endpoints that will potentially be outdated with the coming of 1.4 +- add liquid staked governance (when its built) **UI/UX** -- customize wallet connect modal +- Finish Mobile breakpoints -- threejs liquid metal sphere landing page +- focus on mobile landscape breakpoints, (mainly staking page) -- Mobile breakpoints +- find different assets for side bar -**Defi** +**Mobile Menu** - design -- build +- connect wallet button -**Assets** +- graphic elements -- design +- font size / style / decorations -- build +**DevOps** -**Mobile Menu** +- make onboarding networks seamless + +**Assets Page** + +- claim rewards claim.test.quicksilver.zone/address/current \*/epoch + +- intent query + +- unbonding query + +**Staking Page** + +- check memo intent creation + +- validator route app.quicksilver.zone/staking/chainId/valoperAddress + +- fix succesful transaction on staking modal not taking you to final step diff --git a/web-ui/bun.lockb b/web-ui/bun.lockb index 2c4ddd2e8b2eeb5a4e64c1e7ea49dd6590132a1a..b2b669b6f3102fa0a43098effefdef9af7f777fd 100755 GIT binary patch delta 106899 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 KLow#~%zp!R)hTcQ delta 98705 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 Pb1duT_%-ABmHK}GX&+9s diff --git a/web-ui/components/Airdrop/airdropSection.tsx b/web-ui/components/Airdrop/airdropSection.tsx new file mode 100644 index 000000000..6dd10f012 --- /dev/null +++ b/web-ui/components/Airdrop/airdropSection.tsx @@ -0,0 +1,111 @@ +import { + Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon, + Box, + Button, + Flex, + Text, + Progress, + Tooltip, + VStack, + HStack, + useColorModeValue, + Icon, + Badge, + useDisclosure, +} from '@chakra-ui/react'; +import { CheckIcon, ChevronDownIcon, ChevronRightIcon, InfoOutlineIcon } from '@chakra-ui/icons'; +import { useAccordionStyles } from '@chakra-ui/accordion'; +import { useState } from 'react'; + +interface AirdropAccordionItemProps { + index: number; + defaultIsOpen?: boolean; +} + +const AirdropAccordionItem: React.FC = ({ index, defaultIsOpen }) => { + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen }); + + return ( + +

    + + + {index + 1} + + + {index === 0 ? 'CLAIM INITIAL QCK AIRDROP' : 'ACTION DESCRIPTION'} + + + +

    + {isOpen && ( + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + )} +
    + ); +}; + +const AirdropSection = () => { + const { isOpen, onToggle } = useDisclosure(); + + return ( + + + + + Cosmos Hub Active Airdrop + + + + + + + + {Array.from({ length: 5 }).map((_, index) => ( + + ))} + + + + + + Participate in Other Airdrops + + + + {/* Replace these with actual chain data */} + {['OSMOSIS', 'JUNO', 'STARGAZE', 'REGEN', 'COSMOS'].map((chain) => ( + + + {chain} + + + ))} + + + + ); +}; + +export default AirdropSection; diff --git a/web-ui/components/Assets/assetsGrid.tsx b/web-ui/components/Assets/assetsGrid.tsx index 2df225c59..459d12ac6 100644 --- a/web-ui/components/Assets/assetsGrid.tsx +++ b/web-ui/components/Assets/assetsGrid.tsx @@ -1,14 +1,44 @@ -import { Box, SimpleGrid, VStack, Text, Button, Divider, useColorModeValue, HStack, Flex, Grid, GridItem } from '@chakra-ui/react'; +import { shiftDigits } from '@/utils'; +import { Box, SimpleGrid, VStack, Text, Button, Divider, useColorModeValue, HStack, Flex, Grid, GridItem, Spinner } from '@chakra-ui/react'; import React from 'react'; - +import QDepositModal from './modals/qTokenDepositModal'; +import QWithdrawModal from './modals/qTokenWithdrawlModal'; interface AssetCardProps { assetName: string; balance: string; - apy: string; + apy: number; nativeAssetName: string; + isWalletConnected: boolean; +} + +interface AssetGridProps { + isWalletConnected: boolean; + assets: Array<{ + name: string; + balance: string; + apy: number; + native: string; + }>; } -const AssetCard: React.FC = ({ assetName, balance, apy, nativeAssetName }) => { +const AssetCard: React.FC = ({ assetName, balance, apy, nativeAssetName, isWalletConnected }) => { + if (!balance || !apy) { + return ( + + + + ); + } return ( @@ -21,11 +51,11 @@ const AssetCard: React.FC = ({ assetName, balance, apy, nativeAs APY: - {apy} + {shiftDigits(apy.toFixed(2), 2)}% - + @@ -50,124 +80,55 @@ const AssetCard: React.FC = ({ assetName, balance, apy, nativeAs - - - - - - - - {nativeAssetName} - - - - - - - - - - - - ON QUICKSILVER: - - - - - {balance} - - - - - + + ); }; -const AssetsGrid = () => { - const assets = [ - { name: 'qATOM', balance: '0.123', apy: '12.34%', native: 'ATOM' }, - { name: 'qREGEN', balance: '0.123', apy: '12.34%', native: 'REGEN' }, - { name: 'qOSMO', balance: '0.123', apy: '12.34%', native: 'OSMO' }, - { name: 'qSTARS', balance: '0.123', apy: '12.34%', native: 'STARS' }, - { name: 'qSOMM', balance: '0.123', apy: '12.34%', native: 'SOMM' }, - ]; - +const AssetsGrid: React.FC = ({ assets, isWalletConnected }) => { return ( <> - Assets (qAssets + Native Balance) + qAssets - - - {assets.map((asset, index) => ( - - {' '} - - - ))} + {!isWalletConnected && ( + + + Wallet is not connected! Please connect your wallet to interact with your qAssets. + - + )} + {isWalletConnected && ( + + + {assets.map((asset, index) => ( + + {' '} + + + ))} + + + )} ); }; diff --git a/web-ui/components/Assets/index.ts b/web-ui/components/Assets/index.ts new file mode 100644 index 000000000..72ef2eeb9 --- /dev/null +++ b/web-ui/components/Assets/index.ts @@ -0,0 +1,6 @@ +export * from './modals' +export * from './assetsGrid' +export * from './portfolio' +export * from './intents' +export * from './quickbox' +export * from './unbondingTable' diff --git a/web-ui/components/Assets/intents.tsx b/web-ui/components/Assets/intents.tsx index 32f32304f..286dda92e 100644 --- a/web-ui/components/Assets/intents.tsx +++ b/web-ui/components/Assets/intents.tsx @@ -1,5 +1,5 @@ import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; -import { Box, Flex, Text, Button, IconButton, VStack, Image, Heading, SlideFade } from '@chakra-ui/react'; +import { Box, Flex, Text, Button, IconButton, VStack, Image, Heading, SlideFade, Spinner } from '@chakra-ui/react'; import { color } from 'framer-motion'; import { useState } from 'react'; @@ -7,19 +7,36 @@ import { useIntentQuery } from '@/hooks/useQueries'; export interface StakingIntentProps { address: string; + isWalletConnected: boolean; } -const StakingIntent: React.FC = ({ address }) => { - const { intent, isLoading, isError } = useIntentQuery('cosmoshub', address ?? ''); - +const StakingIntent: React.FC = ({ address, isWalletConnected }) => { const validators = [ { name: 'Validator 1', logo: '/validator1.png', percentage: '30%' }, { name: 'Validator 2', logo: '/validator2.png', percentage: '40%' }, ]; - const chains = ['Cosmos', 'Osmosis', 'Stargaze', 'Regen', 'Sommelier']; + const chains = ['Stargaze', 'Cosmos', 'Osmosis', 'Regen', 'Sommelier']; const [currentChainIndex, setCurrentChainIndex] = useState(0); + const currentChainName = chains[currentChainIndex]; + let newChainName: string | undefined; + if (currentChainName === 'Cosmos') { + newChainName = 'cosmoshub'; + } else if (currentChainName === 'Osmosis') { + newChainName = 'osmosistestnet'; + } else if (currentChainName === 'Stargaze') { + newChainName = 'stargazetestnet'; + } else if (currentChainName === 'Regen') { + newChainName = 'regen'; + } else if (currentChainName === 'Sommelier') { + newChainName = 'sommelier-3'; + } else { + // Default case + newChainName = currentChainName; + } + const { intent, isLoading, isError } = useIntentQuery(newChainName, address ?? ''); + const handleLeftArrowClick = () => { setCurrentChainIndex((prevIndex) => (prevIndex === 0 ? chains.length - 1 : prevIndex - 1)); }; @@ -28,6 +45,34 @@ const StakingIntent: React.FC = ({ address }) => { setCurrentChainIndex((prevIndex) => (prevIndex === chains.length - 1 ? 0 : prevIndex + 1)); }; + if (!isWalletConnected) { + return ( + + + Wallet is not connected. Please connect your wallet to interact with your QCK tokens. + + + ); + } + + if (!intent) { + return ( + + + + ); + } + return ( diff --git a/web-ui/components/Assets/modals/index.ts b/web-ui/components/Assets/modals/index.ts new file mode 100644 index 000000000..1fc5c7fd3 --- /dev/null +++ b/web-ui/components/Assets/modals/index.ts @@ -0,0 +1,4 @@ +export * from './qTokenDepositModal'; +export * from './qTokenWithdrawlModal'; +export * from './qckDepositModal'; +export * from './qckWithdrawModal'; \ No newline at end of file diff --git a/web-ui/components/Assets/modals/qTokenDepositModal.tsx b/web-ui/components/Assets/modals/qTokenDepositModal.tsx new file mode 100644 index 000000000..44ea4f4d8 --- /dev/null +++ b/web-ui/components/Assets/modals/qTokenDepositModal.tsx @@ -0,0 +1,224 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Button, + FormControl, + FormLabel, + Input, + Select, + useDisclosure, + useToast, + Spinner, +} from '@chakra-ui/react'; +import { ChainName } from '@cosmos-kit/core'; +import { useChain, useManager } from '@cosmos-kit/react'; +import { useState, useMemo, useEffect } from 'react'; + +import { ChooseChain } from '@/components/react/choose-chain'; +import { handleSelectChainDropdown, ChainOption } from '@/components/types'; +import { ibc } from 'interchain-query'; +import { useBalanceQuery, useIbcBalanceQuery } from '@/hooks/useQueries'; +import { useTx } from '@/hooks'; +import BigNumber from 'bignumber.js'; +import { getCoin, getIbcInfo } from '@/utils'; +import { StdFee, coins } from '@cosmjs/stargate'; +import { store } from '@interchain-ui/react'; + +export interface QDepositModalProps { + token: string; +} + +const QDepositModal: React.FC = ({ token }) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const toast = useToast(); + + const [chainName, setChainName] = useState('osmosis'); + const { chainRecords, getChainLogo } = useManager(); + const [amount, setAmount] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const chainOptions = useMemo(() => { + const desiredChains = ['osmosis', 'secretnetwork', 'umee']; + return chainRecords + .filter((chainRecord) => desiredChains.includes(chainRecord.name)) + .map((chainRecord) => ({ + chainName: chainRecord?.name, + label: chainRecord?.chain.pretty_name, + value: chainRecord?.name, + icon: getChainLogo(chainRecord.name), + })); + }, [chainRecords, getChainLogo]); + + useEffect(() => { + setChainName(window.localStorage.getItem('selected-chain') || 'osmosis'); + }, []); + + const onChainChange: handleSelectChainDropdown = async (selectedValue: ChainOption | null) => { + setChainName(selectedValue?.chainName); + if (selectedValue?.chainName) { + window?.localStorage.setItem('selected-chain', selectedValue?.chainName); + } else { + window?.localStorage.removeItem('selected-chain'); + } + }; + + const chooseChain = ; + + const fromChain = chainName; + const toChain = 'quicksilver'; + + const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; + const { address, connect, status, message, wallet } = useChain(fromChain ?? ''); + 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); + + const coin = getCoin(fromChain ?? ''); + const transferAmount = new BigNumber(amount).shiftedBy(6).toString(); + + const fee: StdFee = { + amount: coins('1000', coin.base), + gas: '300000', + }; + + const sourcePort = 'transfer'; + const sourceChannel = 'channel-0'; + + const token = { + denom: 'ibc/635CB83EF1DFE598B10A3E90485306FD0D47D34217A4BE5FD9977FA010A5367D', + amount: transferAmount, + }; + + const stamp = Date.now(); + const timeoutInNanos = (stamp + 1.2e6) * 1e6; + + const msg = transfer({ + sourcePort, + sourceChannel, + sender: address ?? '', + receiver: qAddress ?? '', + token, + timeoutHeight: undefined, + //@ts-ignore + timeoutTimestamp: timeoutInNanos, + memo: '', + }); + + await tx([msg], { + fee, + onSuccess: () => { + setAmount(''); + }, + }); + + setIsLoading(false); + }; + + return ( + <> + + + + + + Deposit {token} Tokens + + + {/* Chain Selection Dropdown */} + + From Chain + {chooseChain} + + + {/* Amount Input */} + + Amount + setAmount(e.target.value)} + color={'white'} + placeholder="Enter amount" + /> + + + + + + + + + + + ); +}; + +export default QDepositModal; diff --git a/web-ui/components/Assets/modals/qTokenWithdrawlModal.tsx b/web-ui/components/Assets/modals/qTokenWithdrawlModal.tsx new file mode 100644 index 000000000..a96dcae25 --- /dev/null +++ b/web-ui/components/Assets/modals/qTokenWithdrawlModal.tsx @@ -0,0 +1,224 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Button, + FormControl, + FormLabel, + Input, + Select, + useDisclosure, + useToast, + Spinner, +} from '@chakra-ui/react'; +import { ChainName } from '@cosmos-kit/core'; +import { useChain, useManager } from '@cosmos-kit/react'; +import { useState, useMemo, useEffect } from 'react'; + +import { ChooseChain } from '@/components/react/choose-chain'; +import { handleSelectChainDropdown, ChainOption } from '@/components/types'; +import { ibc } from 'interchain-query'; +import { useBalanceQuery, useIbcBalanceQuery } from '@/hooks/useQueries'; +import { useTx } from '@/hooks'; +import BigNumber from 'bignumber.js'; +import { getCoin, getIbcInfo } from '@/utils'; +import { StdFee, coins } from '@cosmjs/stargate'; +import { store } from '@interchain-ui/react'; + +interface QDepositModalProps { + token: string; +} + +const QWithdrawModal: React.FC = ({ token }) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const toast = useToast(); + + const [chainName, setChainName] = useState('osmosis'); + const { chainRecords, getChainLogo } = useManager(); + const [amount, setAmount] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const chainOptions = useMemo(() => { + const desiredChains = ['osmosis', 'secretnetwork', 'umee']; + return chainRecords + .filter((chainRecord) => desiredChains.includes(chainRecord.name)) + .map((chainRecord) => ({ + chainName: chainRecord?.name, + label: chainRecord?.chain.pretty_name, + value: chainRecord?.name, + icon: getChainLogo(chainRecord.name), + })); + }, [chainRecords, getChainLogo]); + + useEffect(() => { + setChainName(window.localStorage.getItem('selected-chain') || 'osmosis'); + }, []); + + const onChainChange: handleSelectChainDropdown = async (selectedValue: ChainOption | null) => { + setChainName(selectedValue?.chainName); + if (selectedValue?.chainName) { + window?.localStorage.setItem('selected-chain', selectedValue?.chainName); + } else { + window?.localStorage.removeItem('selected-chain'); + } + }; + + const chooseChain = ; + + const fromChain = chainName; + const toChain = 'quicksilver'; + + const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; + const { address, connect, status, message, wallet } = useChain(fromChain ?? ''); + 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); + + const coin = getCoin(fromChain ?? ''); + const transferAmount = new BigNumber(amount).shiftedBy(6).toString(); + + const fee: StdFee = { + amount: coins('1000', coin.base), + gas: '300000', + }; + + const sourcePort = 'transfer'; + const sourceChannel = 'channel-0'; + + const token = { + denom: 'ibc/635CB83EF1DFE598B10A3E90485306FD0D47D34217A4BE5FD9977FA010A5367D', + amount: transferAmount, + }; + + const stamp = Date.now(); + const timeoutInNanos = (stamp + 1.2e6) * 1e6; + + const msg = transfer({ + sourcePort, + sourceChannel, + sender: address ?? '', + receiver: qAddress ?? '', + token, + timeoutHeight: undefined, + //@ts-ignore + timeoutTimestamp: timeoutInNanos, + memo: '', + }); + + await tx([msg], { + fee, + onSuccess: () => { + setAmount(''); + }, + }); + + setIsLoading(false); + }; + + return ( + <> + + + + + + Withdraw {token} Tokens + + + {/* Chain Selection Dropdown */} + + To Chain + {chooseChain} + + + {/* Amount Input */} + + Amount + setAmount(e.target.value)} + color={'white'} + placeholder="Enter amount" + /> + + + + + + + + + + + ); +}; + +export default QWithdrawModal; diff --git a/web-ui/components/Assets/modals/qckDepositModa.tsx b/web-ui/components/Assets/modals/qckDepositModa.tsx deleted file mode 100644 index 5f1ee1dbb..000000000 --- a/web-ui/components/Assets/modals/qckDepositModa.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - ModalCloseButton, - Button, - FormControl, - FormLabel, - Input, - Select, - useDisclosure, - useToast, -} from '@chakra-ui/react'; -import { ChainName } from '@cosmos-kit/core'; -import { useManager } from '@cosmos-kit/react'; -import { useState, useMemo, useEffect } from 'react'; - -import { ChooseChain } from '@/components/react/choose-chain'; -import { handleSelectChainDropdown, ChainOption } from '@/components/types'; - -export function DepositModal() { - const { isOpen, onOpen, onClose } = useDisclosure(); - const toast = useToast(); - - const [chainName, setChainName] = useState('akash'); - const { chainRecords, getChainLogo } = useManager(); - - const chainOptions = useMemo( - () => - chainRecords.map((chainRecord) => { - return { - chainName: chainRecord?.name, - label: chainRecord?.chain.pretty_name, - value: chainRecord?.name, - icon: getChainLogo(chainRecord.name), - }; - }), - [chainRecords, getChainLogo], - ); - - useEffect(() => { - setChainName(window.localStorage.getItem('selected-chain') || 'akash'); - }, []); - - const onChainChange: handleSelectChainDropdown = async (selectedValue: ChainOption | null) => { - setChainName(selectedValue?.chainName); - if (selectedValue?.chainName) { - window?.localStorage.setItem('selected-chain', selectedValue?.chainName); - } else { - window?.localStorage.removeItem('selected-chain'); - } - }; - - const chooseChain = ; - - const handleDeposit = () => { - // Implement deposit logic here - // Show toast on success or error - toast({ - title: 'Deposit Successful', - status: 'success', - duration: 9000, - isClosable: true, - }); - onClose(); // Close modal after deposit - }; - - return ( - <> - - - - - - Deposit QCK Tokens - - - {/* Chain Selection Dropdown */} - - From Chain - {chooseChain} - - - {/* Amount Input */} - - Amount - - - - - - - - - - - - ); -} diff --git a/web-ui/components/Assets/modals/qckDepositModal.tsx b/web-ui/components/Assets/modals/qckDepositModal.tsx new file mode 100644 index 000000000..fda308e7d --- /dev/null +++ b/web-ui/components/Assets/modals/qckDepositModal.tsx @@ -0,0 +1,215 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Button, + FormControl, + FormLabel, + Input, + Select, + useDisclosure, + useToast, + Spinner, +} from '@chakra-ui/react'; +import { ChainName } from '@cosmos-kit/core'; +import { useChain, useManager } from '@cosmos-kit/react'; +import { useState, useMemo, useEffect } from 'react'; + +import { ChooseChain } from '@/components/react/choose-chain'; +import { handleSelectChainDropdown, ChainOption } from '@/components/types'; +import { ibc } from 'interchain-query'; +import { useBalanceQuery, useIbcBalanceQuery } from '@/hooks/useQueries'; +import { useTx } from '@/hooks'; +import BigNumber from 'bignumber.js'; +import { getCoin, getIbcInfo } from '@/utils'; +import { StdFee, coins } from '@cosmjs/stargate'; + +export function DepositModal() { + const { isOpen, onOpen, onClose } = useDisclosure(); + const toast = useToast(); + + const [chainName, setChainName] = useState('osmosis'); + const { chainRecords, getChainLogo } = useManager(); + const [amount, setAmount] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const chainOptions = useMemo(() => { + return chainRecords + .filter((chainRecord) => chainRecord.name === 'osmosis') + .map((chainRecord) => ({ + chainName: chainRecord?.name, + label: chainRecord?.chain.pretty_name, + value: chainRecord?.name, + icon: getChainLogo(chainRecord.name), + })); + }, [chainRecords, getChainLogo]); + + useEffect(() => { + setChainName(window.localStorage.getItem('selected-chain') || 'osmosis'); + }, []); + + const onChainChange: handleSelectChainDropdown = async (selectedValue: ChainOption | null) => { + setChainName(selectedValue?.chainName); + if (selectedValue?.chainName) { + window?.localStorage.setItem('selected-chain', selectedValue?.chainName); + } else { + window?.localStorage.removeItem('selected-chain'); + } + }; + + const chooseChain = ; + + const fromChain = chainName; + const toChain = 'quicksilver'; + + const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; + const { address, connect, status, message, wallet } = useChain(fromChain ?? ''); + 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); + + const coin = getCoin(fromChain ?? ''); + const transferAmount = new BigNumber(amount).shiftedBy(6).toString(); + + const fee: StdFee = { + amount: coins('1000', coin.base), + gas: '300000', + }; + + const sourcePort = 'transfer'; + const sourceChannel = 'channel-0'; + + const token = { + denom: 'ibc/635CB83EF1DFE598B10A3E90485306FD0D47D34217A4BE5FD9977FA010A5367D', + amount: transferAmount, + }; + + const stamp = Date.now(); + const timeoutInNanos = (stamp + 1.2e6) * 1e6; + + const msg = transfer({ + sourcePort, + sourceChannel, + sender: address ?? '', + receiver: qAddress ?? '', + token, + timeoutHeight: undefined, + //@ts-ignore + timeoutTimestamp: timeoutInNanos, + memo: '', + }); + + await tx([msg], { + fee, + onSuccess: () => { + setAmount(''); + }, + }); + + setIsLoading(false); + }; + + return ( + <> + + + + + + Deposit QCK Tokens + + + {/* Chain Selection Dropdown */} + + From Chain + {chooseChain} + + + {/* Amount Input */} + + Amount + setAmount(e.target.value)} + color={'white'} + placeholder="Enter amount" + /> + + + + + + + + + + + ); +} diff --git a/web-ui/components/Assets/modals/qckWithdrawModal.tsx b/web-ui/components/Assets/modals/qckWithdrawModal.tsx index 0116ab15c..6c8990a61 100644 --- a/web-ui/components/Assets/modals/qckWithdrawModal.tsx +++ b/web-ui/components/Assets/modals/qckWithdrawModal.tsx @@ -13,37 +13,44 @@ import { Select, useDisclosure, useToast, + Spinner, } from '@chakra-ui/react'; import { ChainName } from '@cosmos-kit/core'; -import { useManager } from '@cosmos-kit/react'; +import { useChain, useManager } from '@cosmos-kit/react'; import { color } from 'framer-motion'; import { useState, useMemo, useEffect } from 'react'; import { ChooseChain } from '@/components/react/choose-chain'; import { handleSelectChainDropdown, ChainOption } from '@/components/types'; +import { useTx } from '@/hooks'; +import { useIbcBalanceQuery } from '@/hooks/useQueries'; +import { getCoin } from '@/utils'; +import { StdFee, coins } from '@cosmjs/stargate'; +import { ibc } from 'interchain-query'; +import BigNumber from 'bignumber.js'; export function WithdrawModal() { const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); - const [chainName, setChainName] = useState('akash'); + const [chainName, setChainName] = useState('osmosis'); const { chainRecords, getChainLogo } = useManager(); + const [amount, setAmount] = useState(''); + const [isLoading, setIsLoading] = useState(false); - const chainOptions = useMemo( - () => - chainRecords.map((chainRecord) => { - return { - chainName: chainRecord?.name, - label: chainRecord?.chain.pretty_name, - value: chainRecord?.name, - icon: getChainLogo(chainRecord.name), - }; - }), - [chainRecords, getChainLogo], - ); + const chainOptions = useMemo(() => { + return chainRecords + .filter((chainRecord) => chainRecord.name === 'osmosis') + .map((chainRecord) => ({ + chainName: chainRecord?.name, + label: chainRecord?.chain.pretty_name, + value: chainRecord?.name, + icon: getChainLogo(chainRecord.name), + })); + }, [chainRecords, getChainLogo]); useEffect(() => { - setChainName(window.localStorage.getItem('selected-chain') || 'akash'); + setChainName(window.localStorage.getItem('selected-chain') || 'osmosis'); }, []); const onChainChange: handleSelectChainDropdown = async (selectedValue: ChainOption | null) => { @@ -57,16 +64,57 @@ export function WithdrawModal() { const chooseChain = ; - const handleDeposit = () => { - // Implement deposit logic here - // Show toast on success or error - toast({ - title: 'Deposit Successful', - status: 'success', - duration: 9000, - isClosable: true, + const fromChain = 'quicksilver'; + const toChain = chainName; + + const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; + const { address, connect, status, message, wallet } = useChain(toChain ?? ''); + const { address: qAddress } = useChain('quicksilver'); + const { balance } = useIbcBalanceQuery(fromChain ?? '', qAddress ?? ''); + const { tx } = useTx(fromChain ?? ''); + + const onSubmitClick = async () => { + setIsLoading(true); + + const coin = getCoin(fromChain ?? ''); + const transferAmount = new BigNumber(amount).shiftedBy(6).toString(); + + const fee: StdFee = { + amount: coins('1000', coin.base), + gas: '300000', + }; + + const sourcePort = 'transfer'; + const sourceChannel = 'channel-0'; + + const token = { + denom: 'uqck', + amount: transferAmount, + }; + + const stamp = Date.now(); + const timeoutInNanos = (stamp + 1.2e6) * 1e6; + + const msg = transfer({ + sourcePort, + sourceChannel, + sender: qAddress ?? '', + receiver: address ?? '', + token, + timeoutHeight: undefined, + //@ts-ignore + timeoutTimestamp: timeoutInNanos, + memo: '', }); - onClose(); // Close modal after deposit + + await tx([msg], { + fee, + onSuccess: () => { + setAmount(''); + }, + }); + + setIsLoading(false); }; return ( @@ -103,7 +151,25 @@ export function WithdrawModal() { {/* Amount Input */} Amount - + setAmount(e.target.value)} + color={'white'} + placeholder="Enter amount" + /> @@ -118,9 +184,12 @@ export function WithdrawModal() { color: 'complimentary.300', }} mr={3} - onClick={handleDeposit} + minW="100px" + onClick={onSubmitClick} + disabled={Number.isNaN(Number(amount))} > - Withdraw + {isLoading === true && } + {isLoading === false && 'Withdraw'} + ))} + + )} + + + + + + + + + + + + + + {isLoading && !defi && ( + + + + )} + {defi && + filteredData.map((asset, index) => ( + + + + + + + + ))} + {defi && filteredData.length === 0 && ( + + + + )} + +
    + Asset Pair + + APY + + TVL + + Provider + + Action +
    + {' '} + {/* Span across all columns */} +
    + +
    +
    + + + {asset.assetPair} + + + {formatApy(asset.apy)} + + {asset.tvl} + + {asset.provider} + + + + +
    + {' '} + {/* Span across all columns */} +
    + No entries found for this category, please check back later! +
    +
    +
    +
    + ); +}; + +export default DefiTable; diff --git a/web-ui/components/Staking/assetsAccordion.tsx b/web-ui/components/Staking/assetsAccordion.tsx index 79665e04f..765317b0e 100644 --- a/web-ui/components/Staking/assetsAccordion.tsx +++ b/web-ui/components/Staking/assetsAccordion.tsx @@ -22,9 +22,33 @@ export const AssetsAccordian: React.FC = ({ selectedOption const renderQAssets = () => { if (qBalance) { - return qAssetsDisplay; + return ( + + {qAssetsDisplay} + + ); } else { - return ; + return ( + + + + ); + } + }; + + const renderAssets = () => { + if (Number(balance) > 0.000001) { + return ( + + {balanceDisplay} + + ); + } else { + return ( + + + + ); } }; @@ -43,9 +67,7 @@ export const AssetsAccordian: React.FC = ({ selectedOption Available to stake - - {balanceDisplay} - + {renderAssets()} {selectedOption.value.toUpperCase()} @@ -62,9 +84,8 @@ export const AssetsAccordian: React.FC = ({ selectedOption Liquid Staked - - {renderQAssets()} - + + {renderQAssets()} q{selectedOption.value.toUpperCase()} diff --git a/web-ui/components/Staking/infoBox.tsx b/web-ui/components/Staking/infoBox.tsx index f961cdd6b..64e6d8508 100644 --- a/web-ui/components/Staking/infoBox.tsx +++ b/web-ui/components/Staking/infoBox.tsx @@ -19,17 +19,45 @@ export const InfoBox: React.FC = ({ selectedOption, displa const renderRedemptionRate = () => { if (!isZoneLoading && zone) { - return ` 1 q${selectedOption.value.toUpperCase()} = ${Number(zone.redemptionRate).toFixed(2)} ${selectedOption.value.toUpperCase()}`; + return ( + + 1 q{selectedOption.value.toUpperCase()} = {Number(zone.redemptionRate).toFixed(2)} {selectedOption.value.toUpperCase()} + + ); } else { - return ; + return ( + + + + ); + } + }; + + const renderApy = () => { + if (displayApr) { + return ( + + {displayApr} + + ); + } else { + return ( + + + + ); } }; const renderUnbondingPeriod = () => { if (!isZoneLoading && zone) { - return (Number(zone?.unbondingPeriod) / 86400000000000).toString() + ' days'; + return {(Number(zone?.unbondingPeriod) / 86400000000000).toString()} days; } else { - return ; + return ( + + + + ); } }; @@ -60,9 +88,7 @@ export const InfoBox: React.FC = ({ selectedOption, displa Rewards - - {displayApr} - + {renderApy()} @@ -72,8 +98,6 @@ export const InfoBox: React.FC = ({ selectedOption, displa - {' '} - {/* Adjusts right margin */} @@ -87,23 +111,21 @@ export const InfoBox: React.FC = ({ selectedOption, displa -

    - - - - {' '} - {/* Adjusts right margin */} - - - - Unbonding - - - - {renderUnbondingPeriod()} + + + + {' '} + {/* Adjusts right margin */} + + + + Unbonding -

    + + {renderUnbondingPeriod()} +
    + @@ -118,23 +140,20 @@ export const InfoBox: React.FC = ({ selectedOption, displa -

    - - - - {' '} - {/* Adjusts right margin */} - - - - Value of 1 q{selectedOption.value.toUpperCase()} - - - - {renderRedemptionRate()} + + + + {' '} + {/* Adjusts right margin */} + + + + Value of 1 q{selectedOption.value.toUpperCase()} -

    + + {renderRedemptionRate()} +
    diff --git a/web-ui/components/Staking/modals/stakingProcessModal.tsx b/web-ui/components/Staking/modals/stakingProcessModal.tsx index 714f45b59..85ecc74ed 100644 --- a/web-ui/components/Staking/modals/stakingProcessModal.tsx +++ b/web-ui/components/Staking/modals/stakingProcessModal.tsx @@ -26,7 +26,7 @@ import React, { useEffect, useState } from 'react'; import { MultiModal } from './validatorSelectionModal'; -import { useQueryHooks } from '@/hooks'; +import { useQueryHooks, useTx } from '@/hooks'; import { useZoneQuery } from '@/hooks/useQueries'; import { liquidStakeTx, unbondLiquidStakeTx } from '@/tx/liquidStakeTx'; @@ -83,7 +83,21 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo const [isSigning, setIsSigning] = useState(false); const [isError, setIsError] = useState(false); - const { address, getSigningStargateClient } = useChain(selectedOption?.chainName || ''); + let newChainName: string | undefined; + if (selectedOption?.chainId === 'provider') { + newChainName = 'cosmoshub'; + } else if (selectedOption?.chainId === 'elgafar-1') { + newChainName = 'stargazetestnet'; + } else if (selectedOption?.chainId === 'osmo-test-5') { + newChainName = 'osmosistestnet'; + } else if (selectedOption?.chainId === 'regen-redwood-1') { + newChainName = 'regen'; + } else { + // Default case + newChainName = selectedOption?.chainName; + } + + const { address, getSigningStargateClient } = useChain(newChainName || ''); const labels = ['Choose validators', `Set weights`, `Sign & Submit`, `Receive q${selectedOption?.value}`]; const [isModalOpen, setModalOpen] = useState(false); @@ -168,7 +182,7 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo const response = await liquidStakeTx( getSigningStargateClient, setResp, - selectedOption?.chainName || '', + newChainName || '', selectedOption?.chainId || '', address, toast, @@ -233,7 +247,11 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo }; const handleStepOneButtonClick = () => { - if (check) { + // Check if only one validator is selected + if (selectedValidators.length === 1) { + setUseDefaultWeights(true); + setStep(3); // Skip directly to step 3 + } else if (check) { // If checkbox is checked, skip directly to step 3 setStep(3); } else { @@ -344,15 +362,30 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo mt={4} width="55%" _hover={{ - bgColor: '#181818', + bgColor: 'complimentary.500', }} onClick={handleStepOneButtonClick} > - {check ? 'Skip to Step 3' : selectedValidators.length > 0 ? 'Next' : 'Choose Validators'} + {check ? 'Sign & Submit' : selectedValidators.length > 0 ? 'Next' : 'Choose Validators'} {selectedValidators.length === 0 && ( - + Proceed with existing intent? )} @@ -376,14 +409,26 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo - + {selectedValidators.length > 1 && ( + + )} diff --git a/web-ui/components/ThreeJS/liquidMetalSphere.tsx b/web-ui/components/ThreeJS/liquidMetalSphere.tsx index 75b1a0d1e..411398285 100644 --- a/web-ui/components/ThreeJS/liquidMetalSphere.tsx +++ b/web-ui/components/ThreeJS/liquidMetalSphere.tsx @@ -1,55 +1,92 @@ import { useRef, useEffect } from 'react'; import * as THREE from 'three'; +import { ImprovedNoise } from 'three/examples/jsm/math/ImprovedNoise.js'; const LiquidMetalSphere = () => { - const mountRef = useRef( - null, - ); + const mountRef = useRef(null); useEffect(() => { if (!mountRef.current) return; - + const mouse = new THREE.Vector2(); const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera( - 75, - window.innerWidth / window.innerHeight, - 1, - 10, - ); - const renderer = new THREE.WebGLRenderer({ - antialias: true, - }); + const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 100); + const renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + mountRef.current.appendChild(renderer.domElement); - renderer.setSize( - window.innerWidth, - window.innerHeight, - ); - mountRef.current.appendChild( - renderer.domElement, - ); + // Increase the radius to make the sphere larger + const geometry = new THREE.SphereGeometry(4, 64, 64); // Radius increased to 3 + const vertexShader = ` + varying vec3 vNormal; + varying vec3 vPosition; + varying vec2 vUv; + void main() { + vNormal = normalize(normalMatrix * normal); + vPosition = position; + vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0); + vUv = vec2(modelViewPosition.x, modelViewPosition.y) / 10.0 + 0.5; + gl_Position = projectionMatrix * modelViewPosition; + } + `; + const fragmentShader = ` + uniform float time; + uniform vec2 mousePos; + uniform vec2 resolution; + varying vec3 vNormal; + varying vec3 vPosition; + varying vec2 vUv; + void main() { + vec3 orange = vec3(1.0, 0.55, 0.0); + vec3 darkArea = vec3(0.1, 0.1, 0.1); + float dist = distance(gl_PointCoord.xy, mousePos); + float ripple = sin(dist * 10.0 - time * 5.0) * 0.5 + 0.5; + vec3 color = mix(orange, darkArea, ripple); + gl_FragColor = vec4(color, 64.0); + } + `; + const material = new THREE.ShaderMaterial({ + uniforms: { + time: { value: 1.0 }, + mousePos: { value: new THREE.Vector2(-1, -1) }, // Initialize outside the screen + resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, + }, + vertexShader, + fragmentShader, + }); - const geometry = new THREE.SphereGeometry( - 1, - 32, - 32, - ); - const material = new THREE.MeshBasicMaterial({ - color: 'orange', - wireframe: true, - }); - const sphere = new THREE.Mesh( - geometry, - material, - ); + const sphere = new THREE.Mesh(geometry, material); scene.add(sphere); + camera.position.z = 10; // Adjust camera distance to fit the larger sphere - camera.position.z = 5; + const noise = new ImprovedNoise(); + const positionAttribute = geometry.getAttribute('position'); + const originalPosition: any[] = []; + for (let i = 0; i < positionAttribute.count; i++) { + originalPosition.push(new THREE.Vector3().fromBufferAttribute(positionAttribute, i)); + } + const onMouseMove = (event: { clientX: number; clientY: number }) => { + // Normalize mouse coordinates and update uniform + material.uniforms.mousePos.value.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1); + }; + + window.addEventListener('mousemove', onMouseMove); const animate = () => { requestAnimationFrame(animate); - sphere.rotation.x += 0.001; - sphere.rotation.y += 0.001; + const time = Date.now() * 0.001; + sphere.material.uniforms.time.value = time; + + for (let i = 0; i < positionAttribute.count; i++) { + const vertex = originalPosition[i]; + const offset = noise.noise(vertex.x + time, vertex.y, vertex.z); + const newPosition = vertex.clone().multiplyScalar(1 + offset * 0.03); + positionAttribute.setXYZ(i, newPosition.x, newPosition.y, newPosition.z); + } + positionAttribute.needsUpdate = true; + + sphere.rotation.x += 0.005; + sphere.rotation.y += 0.005; renderer.render(scene, camera); }; @@ -57,10 +94,8 @@ const LiquidMetalSphere = () => { return () => { renderer.dispose(); - window.removeEventListener( - 'resize', - () => {}, - ); + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('resize', () => {}); }; }, []); diff --git a/web-ui/components/react/chain-dropdown.tsx b/web-ui/components/react/chain-dropdown.tsx index e15a7c434..d442b229f 100644 --- a/web-ui/components/react/chain-dropdown.tsx +++ b/web-ui/components/react/chain-dropdown.tsx @@ -33,6 +33,19 @@ const SelectOptions = ({ data, value, onChange }: ChangeChainMenuType) => { control: (provided: SystemStyleObject) => ({ ...provided, height: 12, + _active: { + borderColor: 'complimentary.900', + }, + _selected: { + borderColor: 'complimentary.900', + }, + _hover: { + borderColor: 'complimentary.900', + }, + _focus: { + borderColor: 'complimentary.900', + boxShadow: '0 0 0 3px #FF8000', + }, }), menu: (provided: SystemStyleObject) => ({ ...provided, @@ -43,6 +56,15 @@ const SelectOptions = ({ data, value, onChange }: ChangeChainMenuType) => { boxShadow: '0 1px 5px #FF8000', borderRadius: '0.3rem', color: 'white', + _active: { + borderColor: 'complimentary.900', + }, + _selected: { + borderColor: 'complimentary.900', + }, + _hover: { + borderColor: 'complimentary.900', + }, }), menuList: (provided: SystemStyleObject) => ({ ...provided, @@ -52,6 +74,19 @@ const SelectOptions = ({ data, value, onChange }: ChangeChainMenuType) => { borderRadius: 'none', color: 'white', p: 2, + _active: { + borderColor: 'complimentary.900', + }, + _selected: { + borderColor: 'complimentary.900', + }, + _hover: { + borderColor: 'complimentary.900', + }, + _focus: { + borderColor: 'complimentary.900', + boxShadow: '0 0 0 3px #FF8000', + }, // For Firefox scrollbarWidth: 'auto', scrollbarColor: useColorModeValue('rgba(0,0,0,0.3) rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2) rgba(255,255,255,0.1)'), @@ -90,8 +125,15 @@ const SelectOptions = ({ data, value, onChange }: ChangeChainMenuType) => { mt: 2, }, _active: { - bg: 'primary.50', + bg: 'complimentary.900', }, + _hover: { + bg: 'complimentary.700', + }, + _selected: { + bg: 'complimentary.900', + }, + _disabled: { bg: 'transparent', _hover: { bg: 'transparent' } }, }; }, @@ -102,7 +144,15 @@ const SelectOptions = ({ data, value, onChange }: ChangeChainMenuType) => { const DropdownIndicator = ({ ...props }: DropdownIndicatorProps>) => { return ( - + ); }; diff --git a/web-ui/components/react/sideHeader.tsx b/web-ui/components/react/sideHeader.tsx index 6fa2a617b..740f13962 100644 --- a/web-ui/components/react/sideHeader.tsx +++ b/web-ui/components/react/sideHeader.tsx @@ -33,7 +33,7 @@ export const SideHeader = () => { useEffect(() => { const handleRouteChange = (url: string) => { - const path = url.split('/quicksilver-app-v2/')[1]; + const path = url.split('/quicksilver/')[1]; setSelectedPage(path); }; @@ -44,7 +44,22 @@ export const SideHeader = () => { const commonBoxShadowColor = 'rgba(255, 128, 0, 0.25)'; const toggleSocialLinks = () => setShowSocialLinks(!showSocialLinks); - const isMobile = useBreakpointValue({ base: true, md: false }); + const [isMobile, setIsMobile] = useState(typeof window !== 'undefined' && window.innerWidth < 1274); + + // Handle window resize + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 1274); + }; + + // Set up event listener + window.addEventListener('resize', handleResize); + + // Clean up + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); const transitionStyle = 'all 0.3s ease'; const { isOpen, onOpen, onClose } = useDisclosure(); @@ -52,7 +67,7 @@ export const SideHeader = () => { if (isMobile) { onOpen(); } else { - router.push('/'); + router.push('/staking'); } }; @@ -77,23 +92,22 @@ export const SideHeader = () => { return ( - + logo { - + - - Quicksilver + + QUICKSILVER {['Staking', 'Governance', 'Defi', 'Assets'].map((item) => ( { }} alt="Staking" h="55px" - src="/quicksilver-app-v2/img/test.png" + src="/quicksilver/img/test.png" /> @@ -235,7 +249,7 @@ export const SideHeader = () => { alt="Governance" h="60px" w={'60px'} - src="/quicksilver-app-v2/img/test2.png" + src="/quicksilver/img/test2.png" /> @@ -262,7 +276,33 @@ export const SideHeader = () => { }} alt="Assets" h="55px" - src="/quicksilver-app-v2/img/test3.png" + src="/quicksilver/img/test3.png" + /> + + + + router.push('/airdrop')} + cursor="pointer" + borderRadius="100px" + boxShadow={ + selectedPage === 'aidrop' ? `0 0 15px 5px ${commonBoxShadowColor}, inset 0 0 50px 5px ${commonBoxShadowColor}` : '' + } + _hover={{ + boxShadow: `0 0 15px 5px ${commonBoxShadowColor}, inset 0 0 50px 5px ${commonBoxShadowColor}`, + transition: transitionStyle, + }} + > + DeFi @@ -289,7 +329,7 @@ export const SideHeader = () => { }} alt="DeFi" h="55px" - src="/quicksilver-app-v2/img/test4.png" + src="/quicksilver/img/test4.png" /> diff --git a/web-ui/hooks/index.ts b/web-ui/hooks/index.ts index fb75baa60..1db3f723b 100644 --- a/web-ui/hooks/index.ts +++ b/web-ui/hooks/index.ts @@ -3,4 +3,4 @@ export * from './useToaster'; export * from './useQueryHooks'; export * from './useVotingData'; export * from './useRpcQueryClient'; -export * from './useValidatorData'; + diff --git a/web-ui/hooks/useGrpcQueryClient.ts b/web-ui/hooks/useGrpcQueryClient.ts index 58e66a959..41902cb5e 100644 --- a/web-ui/hooks/useGrpcQueryClient.ts +++ b/web-ui/hooks/useGrpcQueryClient.ts @@ -1,25 +1,30 @@ -import { HttpEndpoint } from '@cosmjs/stargate'; import { quicksilver } from '@hoangdv2429/quicksilverjs'; import { useQuery } from '@tanstack/react-query'; -import { useQueryHooks } from './useQueryHooks'; const createGrpcGateWayClient = quicksilver.ClientFactory.createGrpcGateWayClient; export const useGrpcQueryClient = (chainName: string) => { - let grpcEndpoint: string | HttpEndpoint | undefined; - const solution = useQueryHooks(chainName); - - // Custom logic for setting rpcEndpoint based on the chain name - if (chainName === 'quicksilver') { - grpcEndpoint = 'http://135.181.140.225:1317'; - } else if (chainName === 'cosmoshub') { - grpcEndpoint = 'https://rest.sentry-01.theta-testnet.polypore.xyz'; - } else { - grpcEndpoint = solution.rpcEndpoint; - } - - grpcEndpoint = solution.rpcEndpoint; + + + let grpcEndpoint: string | undefined; + const env = process.env.NEXT_PUBLIC_CHAIN_ENV; + + + + 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, + }; + + + grpcEndpoint = endpoints[chainName]; + + const grpcQueryClientQuery = useQuery({ queryKey: ['grpcQueryClient', grpcEndpoint], diff --git a/web-ui/hooks/useQueries.ts b/web-ui/hooks/useQueries.ts index db237cb93..b2c1744c4 100644 --- a/web-ui/hooks/useQueries.ts +++ b/web-ui/hooks/useQueries.ts @@ -6,8 +6,8 @@ import { cosmos } from 'interchain-query'; import { useGrpcQueryClient } from './useGrpcQueryClient'; -import { getCoin } from '@/utils'; -import { parseValidators } from '@/utils/staking'; +import { getCoin, getLogoUrls } from '@/utils'; +import { ExtendedValidator, parseValidators } from '@/utils/staking'; const BigNumber = require('bignumber.js'); @@ -31,7 +31,42 @@ export const useBalanceQuery = (chainName: string, address: string) => { return balance; }, { - enabled: !!grpcQueryClient, + enabled: !!grpcQueryClient && !!address, + staleTime: Infinity, + }, + ); + + return { + balance: balanceQuery.data, + isLoading: balanceQuery.isLoading, + isError: balanceQuery.isError, + }; +}; + +export const useIbcBalanceQuery = (chainName: string, address: string) => { + const { grpcQueryClient } = useGrpcQueryClient(chainName); + const balanceQuery = useQuery( + ['balance', address], + async () => { + if (!grpcQueryClient) { + throw new Error('RPC Client not ready'); + } + const nextKey = new Uint8Array() + const balance = await grpcQueryClient.cosmos.bank.v1beta1.allBalances({ + address: address || '', + pagination: { + key: nextKey, + offset: Long.fromNumber(0), + limit: Long.fromNumber(100), + countTotal: true, + reverse: false, + }, + }); + + return balance; + }, + { + enabled: !!grpcQueryClient && !!address, staleTime: Infinity, }, ); @@ -77,7 +112,7 @@ export const useQBalanceQuery = (chainName: string, address: string, qAsset: str return balance; }, { - enabled: !!grpcQueryClient, + enabled: !!grpcQueryClient && !!address, staleTime: Infinity, }, ); @@ -100,12 +135,12 @@ export const useIntentQuery = (chainName: string, address: string) => { throw new Error('RPC Client not ready'); } - const intent = await axios.get(`https://quicksilver-rest.publicnode.com/quicksilver/interchainstaking/v1/zones/${chainId}/delegator_intent/${address}`) + const intent = await axios.get(`https://lcd.test.quicksilver.zone/quicksilver/interchainstaking/v1/zones/${chainId}/delegator_intent/${address}`) return intent; }, { - enabled: !!grpcQueryClient, + enabled: !!grpcQueryClient && !!address, staleTime: Infinity, }, ); @@ -117,6 +152,46 @@ export const useIntentQuery = (chainName: string, address: string) => { }; }; +export const useUnbondingQuery = (chainName: string, address: string) => { + const { grpcQueryClient } = useGrpcQueryClient('quicksilver'); + const { chain } = useChain(chainName); + const chainId = chain.chain_id; + const unbondingQuery = useQuery( + ['unbond', chainName], + async () => { + if (!grpcQueryClient) { + throw new Error('RPC Client not ready'); + } + const nextKey = new Uint8Array() + const unbonding = await grpcQueryClient.quicksilver.interchainstaking.v1.withdrawalRecords({ + delegatorAddress: address, + chainId: chainId, + pagination: { + key: nextKey, + offset: Long.fromNumber(0), + limit: Long.fromNumber(100), + countTotal: true, + reverse: false, + }, + + }); + + return unbonding; + + }, + { + enabled: !!grpcQueryClient && !!address, + staleTime: Infinity, + }, + ); + + return { + unbondingData: unbondingQuery.data, + isLoading: unbondingQuery.isLoading, + isError: unbondingQuery.isError, + }; +}; + export const useValidatorsQuery = (chainName: string) => { const { grpcQueryClient } = useGrpcQueryClient(chainName); @@ -243,3 +318,100 @@ export const useZoneQuery = (chainId: string) => { } ); }; + +export const useValidatorLogos = ( + chainName: string, + validators: ExtendedValidator[] +) => { + const { data, isLoading } = useQuery({ + queryKey: ['validatorLogos', chainName], + queryFn: () => getLogoUrls(validators, chainName), + enabled: validators.length > 0, + staleTime: Infinity, + }); + + return { data, isLoading }; +}; + +export const useMissedBlocks = (chainName: string) => { + const { grpcQueryClient } = useGrpcQueryClient(chainName); + + const fetchMissedBlocks = async () => { + if (!grpcQueryClient) { + throw new Error('RPC Client not ready'); + } + + let allMissedBlocks: any[] = []; + let nextKey = new Uint8Array(); + + do { + const response = await grpcQueryClient.cosmos.slashing.v1beta1.signingInfos({ + pagination: { + key: nextKey, + offset: Long.fromNumber(0), + limit: Long.fromNumber(100), + countTotal: true, + reverse: false, + }, + }); + + // Filter out entries without an address + const filteredMissedBlocks = response.info.filter(block => { + const hasAddress = block.address && block.address.trim() !== ''; + const notTombstoned = !block.tombstoned; + const notJailed = new Date(block.jailed_until) <= new Date(); + return hasAddress && notTombstoned && notJailed; + }); + + allMissedBlocks = allMissedBlocks.concat(filteredMissedBlocks); + nextKey = response.pagination.next_key; + } while (nextKey && nextKey.length > 0); + + return allMissedBlocks; + }; + + const missedBlocksQuery = useQuery({ + queryKey: ['missedBlocks', chainName], + queryFn: fetchMissedBlocks, + enabled: !!grpcQueryClient, + staleTime: Infinity, + onError: (error) => { + console.error('Error in fetching Missed Blocks:', error); + }, + }); + + return { + missedBlocksData: missedBlocksQuery.data, + isLoading: missedBlocksQuery.isLoading, + isError: missedBlocksQuery.isError, + }; +}; +interface DefiData { + assetPair: string; + provider: string; + action: string; + apy: number; + tvl: number; + link: string; +} +export const useDefiData = () => { + const query = useQuery( + ['defi'], + async () => { + const res = await axios.get('https://data.test.quicksilver.zone/defi'); + if (!res.data || res.data.length === 0) { + throw new Error('Failed to query defi'); + } + return res.data; + }, + { + staleTime: Infinity, + } + ); + + return { + defi: query.data, + isLoading: query.isLoading, + isError: query.isError, + }; +}; \ No newline at end of file diff --git a/web-ui/hooks/useQueryHooks2.ts b/web-ui/hooks/useQueryHooks2.ts index 22f3f0452..98cdccbef 100644 --- a/web-ui/hooks/useQueryHooks2.ts +++ b/web-ui/hooks/useQueryHooks2.ts @@ -24,10 +24,13 @@ export const useQueryHooks = ( }, }); + + const rpcEndpoint = chainName === 'quicksilver' ? 'https://rpc.quicksilver.zone' : rpcEndpointQuery.data; + const rpcClientQuery = useRpcClient({ - rpcEndpoint: rpcEndpointQuery.data || '', + rpcEndpoint: rpcEndpoint || '', options: { - enabled: !!rpcEndpointQuery.data, + enabled: !!rpcEndpoint, staleTime: Infinity, queryKeyHashFn: (queryKey) => { return JSON.stringify( @@ -53,6 +56,6 @@ export const useQueryHooks = ( cosmosQuery, isReady, isFetching, - rpcEndpoint: rpcEndpointQuery.data, + rpcEndpoint: rpcEndpoint, }; }; \ No newline at end of file diff --git a/web-ui/hooks/useRpcQueryClient.ts b/web-ui/hooks/useRpcQueryClient.ts index 0d2ab6eb5..2870eed66 100644 --- a/web-ui/hooks/useRpcQueryClient.ts +++ b/web-ui/hooks/useRpcQueryClient.ts @@ -8,22 +8,25 @@ const createRPCQueryClient = cosmos.ClientFactory.createRPCQueryClient; export const useRpcQueryClient = (chainName: string) => { let rpcEndpoint: string | HttpEndpoint | undefined; - const solution = useQueryHooks(chainName); - // Custom logic for setting rpcEndpoint based on the chain name - if (chainName === 'quicksilver') { - rpcEndpoint = 'https://rpc.quicksilver.zone'; - } else if (chainName === 'cosmoshub') { - rpcEndpoint = 'https://rpc.sentry-01.theta-testnet.polypore.xyz'; - } else { - rpcEndpoint = solution.rpcEndpoint; - } + const env = process.env.NEXT_PUBLIC_CHAIN_ENV; + + 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, + }; + + rpcEndpoint = endpoints[chainName]; const rpcQueryClientQuery = useQuery({ queryKey: ['rpcQueryClient', rpcEndpoint], queryFn: () => { - return createRPCQueryClient({ rpcEndpoint: rpcEndpoint || 'https://lcd.quicksilver.zone' }); + return createRPCQueryClient({ rpcEndpoint: rpcEndpoint || '' }); }, enabled: !!rpcEndpoint, staleTime: Infinity, diff --git a/web-ui/hooks/useToaster.tsx b/web-ui/hooks/useToaster.tsx index 4f1595b0f..c9c776e4c 100644 --- a/web-ui/hooks/useToaster.tsx +++ b/web-ui/hooks/useToaster.tsx @@ -1,8 +1,4 @@ -import { - useToast, - Text, - Box, -} from '@chakra-ui/react'; +import { useToast, Text, Box, Link } from '@chakra-ui/react'; export enum ToastType { Info = 'info', @@ -17,35 +13,49 @@ export type CustomToast = { message?: string | JSX.Element; closable?: boolean; duration?: number; + txHash?: string; + chainName?: string; }; - export const useToaster = () => { const toast = useToast({ position: 'top-right', containerStyle: { - maxWidth: '150px', + maxWidth: '300px', }, }); - const customToast = ({ - type, - title, - message, - closable = true, - duration = 5000, - }: CustomToast) => { - return toast({ - title, - duration, - status: type, - isClosable: closable, - description: ( + const customToast = ({ type, title, message, closable = true, duration = 5000, txHash, chainName }: CustomToast) => { + let description; + + if (type === ToastType.Success && txHash) { + const mintscanUrl = `https://www.mintscan.io/${chainName}/txs/${txHash}`; + description = ( + + + {message} + + + View on Mintscan + + + ); + } else { + description = ( {message} - ), + ); + } + + return toast({ + position: 'bottom-right', + title, + duration, + status: type, + isClosable: closable, + description, }); }; diff --git a/web-ui/hooks/useTx.ts b/web-ui/hooks/useTx.tsx similarity index 70% rename from web-ui/hooks/useTx.ts rename to web-ui/hooks/useTx.tsx index a1f440fbf..30c98e6f4 100644 --- a/web-ui/hooks/useTx.ts +++ b/web-ui/hooks/useTx.tsx @@ -1,10 +1,19 @@ -import { ToastId } from '@chakra-ui/react'; +import { Box, color, Link, ToastId, useToast, Text } from '@chakra-ui/react'; import { isDeliverTxSuccess, StdFee } from '@cosmjs/stargate'; import { useChain } from '@cosmos-kit/react'; import { cosmos } from 'interchain-query'; import { TxRaw } from 'interchain-query/cosmos/tx/v1beta1/tx'; import { useToaster, ToastType, type CustomToast } from './useToaster'; +import { ChainName } from '@cosmos-kit/core'; +import { on } from 'events'; +import { isExternal } from 'util/types'; + +interface TxResult { + success: boolean; + txHash?: string; + error?: string; +} interface Msg { typeUrl: string; @@ -13,6 +22,7 @@ interface Msg { export interface TxOptions { fee?: StdFee | null; + memo?: string; toast?: Partial; onSuccess?: () => void; } @@ -26,19 +36,18 @@ export enum TxStatus { const txRaw = cosmos.tx.v1beta1.TxRaw; export const useTx = (chainName: string) => { - const { address, getSigningStargateClient, estimateFee } = - useChain(chainName); + const { address, getSigningStargateClient, estimateFee } = useChain(chainName); const toaster = useToaster(); - const tx = async (msgs: Msg[], options: TxOptions) => { + const tx = async (msgs: Msg[], options: TxOptions): Promise => { if (!address) { toaster.toast({ type: ToastType.Error, title: 'Wallet not connected', message: 'Please connect your wallet', }); - return; + return { success: false, error: 'Wallet not connected' }; } let signed: TxRaw; @@ -50,10 +59,7 @@ export const useTx = (chainName: string) => { fee = options.fee; client = await getSigningStargateClient(); } else { - const [_fee, _client] = await Promise.all([ - estimateFee(msgs), - getSigningStargateClient(), - ]); + const [_fee, _client] = await Promise.all([estimateFee(msgs), getSigningStargateClient()]); fee = _fee; client = _client; } @@ -65,7 +71,7 @@ export const useTx = (chainName: string) => { message: e?.message || 'An unexpected error has occured', type: ToastType.Error, }); - return; + return { success: false, error: 'An unexpected error has occured' }; } let broadcastToastId: ToastId; @@ -88,7 +94,10 @@ export const useTx = (chainName: string) => { title: options.toast?.title || TxStatus.Successful, type: options.toast?.type || ToastType.Success, message: options.toast?.message, + chainName: chainName, + txHash: res?.transactionHash, }); + return { success: true, txHash: res.transactionHash }; } else { toaster.toast({ title: TxStatus.Failed, @@ -96,6 +105,7 @@ export const useTx = (chainName: string) => { type: ToastType.Error, duration: 10000, }); + return { success: false, error: res.rawLog }; } }) .catch((err) => { @@ -105,10 +115,13 @@ export const useTx = (chainName: string) => { type: ToastType.Error, duration: 10000, }); + return { success: false, error: err.message }; }) .finally(() => toaster.close(broadcastToastId)); + return { success: false, error: 'Client not initialized or transaction not signed' }; } else { toaster.close(broadcastToastId); + return { success: false, error: 'Client not initialized or transaction not signed' }; } }; diff --git a/web-ui/hooks/useValidatorData.ts b/web-ui/hooks/useValidatorData.ts deleted file mode 100644 index b3821e4e9..000000000 --- a/web-ui/hooks/useValidatorData.ts +++ /dev/null @@ -1,75 +0,0 @@ -import BigNumber from 'bignumber.js'; -import { cosmos } from 'interchain-query'; -import { useEffect, useMemo, useState } from 'react'; - -import { parseValidators } from '@/utils/staking'; - -import { useQueryHooks } from './useQueryHooks'; -import { useRpcQueryClient } from './useRpcQueryClient'; - -(BigInt.prototype as any).toJSON = function () { - return this.toString(); -}; - -export const useValidatorData = (chainName: string) => { - const [isLoading, setIsLoading] = useState(false); - - const { rpcQueryClient } = useRpcQueryClient(chainName); - - const { cosmosQuery, isReady } = useQueryHooks(chainName); - - const validatorsQuery = cosmosQuery.staking.v1beta1.useValidators({ - request: { - status: cosmos.staking.v1beta1.bondStatusToJSON(cosmos.staking.v1beta1.BondStatus.BOND_STATUS_BONDED), - pagination: { - key: new Uint8Array(), - offset: 0n, - limit: 200n, - countTotal: true, - reverse: false, - }, - }, - options: { - queryKey: ['validators', chainName], - enabled: !!rpcQueryClient?.cosmos?.staking?.v1beta1.validator, - select: ({ validators }) => { - const sorted = validators.sort((a, b) => new BigNumber(b.tokens).minus(a.tokens).toNumber()); - return parseValidators(sorted); - }, - onError: (error) => { - console.error('Error fetching validators:', error); - validatorsQuery.remove(); - validatorsQuery.refetch(); - }, - }, - }); - - const loading = validatorsQuery.isFetching || !isReady; - - useEffect(() => { - setIsLoading(loading); - }, [loading]); - - type SingleQueriesData = { - validators: NonNullable<(typeof validatorsQuery)['data']>; - }; - - const singleQueriesData = useMemo(() => { - if (validatorsQuery.isFetching || !isReady) return; - return { - validators: validatorsQuery.data, - } as SingleQueriesData; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [validatorsQuery.isFetching, isReady]); - - const refetch = () => { - validatorsQuery.remove(); - validatorsQuery.refetch(); - }; - - return { - data: singleQueriesData, - isLoading, - refetch, - }; -}; diff --git a/web-ui/next.config.js b/web-ui/next.config.js index 67dede20d..cfa95afb9 100644 --- a/web-ui/next.config.js +++ b/web-ui/next.config.js @@ -3,8 +3,17 @@ module.exports = { reactStrictMode: true, swcMinify: true, - basePath: '/quicksilver-app-v2', - assetPrefix: '/quicksilver-app-v2', + basePath: '/quicksilver', + assetPrefix: '/quicksilver', + async redirects() { + return [ + { + source: '/', + destination: '/staking', + permanent: true, // Change to false if temporary redirect + }, + ]; + }, typescript: { // !! WARN !! // Dangerously allow production builds to successfully complete even if diff --git a/web-ui/package.json b/web-ui/package.json index 0dc16f181..aade4053f 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -2,7 +2,7 @@ "name": "bun-qs", "version": "0.15.3", "private": true, - "homepage": "https://quicksilver-zone.github.io/quicksilver-app-v2/", + "homepage": "https://quicksilver-zone.github.io/quicksilver/", "scripts": { "build": "next build", "dev": "next dev", @@ -11,14 +11,16 @@ "export": "next export" }, "dependencies": { + "@chain-registry/assets": "^1.26.0", "@chakra-ui/icons": "^2.0.12", "@chakra-ui/react": "2.5.1", "@chakra-ui/system": "^2.1.3", + "@chalabi/chain-registry": "1.25.2", "@chalabi/quicksilverjs": "0.0.2", "@cosmjs/amino": "0.28.0", "@cosmjs/cosmwasm-stargate": "0.29.5", "@cosmjs/proto-signing": "0.28.0", - "@cosmjs/stargate": "0.29.5", + "@cosmjs/stargate": "0.32.2", "@cosmos-kit/core": "^2.0.3", "@cosmos-kit/cosmostation": "^2.0.3", "@cosmos-kit/keplr": "^2.0.3", @@ -32,12 +34,14 @@ "@radix-ui/react-icons": "^1.3.0", "@tanstack/react-query": "^4.29.12", "@tanstack/react-query-devtools": "^4.35.0", + "@types/crypto-js": "^4.2.1", "bech32": "^2.0.0", "bun": "^1.0.3", "bun-framework-next": "latest", "chain-registry": "1.19.0", "chakra-react-select": "^4.7.6", "cosmjs-types": "0.5.0", + "crypto-js": "^4.2.0", "dayjs": "^1.11.9", "fast-fuzzy": "^1.12.0", "framer-motion": "9.0.7", diff --git a/web-ui/pages/_app.tsx b/web-ui/pages/_app.tsx index afa9cdc7e..776432235 100644 --- a/web-ui/pages/_app.tsx +++ b/web-ui/pages/_app.tsx @@ -7,28 +7,30 @@ import { SignerOptions, WalletViewProps } from '@cosmos-kit/core'; 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 } from '@cosmos-kit/react'; +import { ChainProvider, ThemeCustomizationProps } from '@cosmos-kit/react'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { chains, assets } from 'chain-registry'; +import { chains, assets } from '@chalabi/chain-registry'; import { cosmosAminoConverters, cosmosProtoRegistry } from 'interchain-query'; import type { AppProps } from 'next/app'; import { quicksilverProtoRegistry, quicksilverAminoConverters } from 'quicksilverjs'; +import { ibcAminoConverters, ibcProtoRegistry } from 'interchain-query'; import { Header, SideHeader } from '@/components'; import { defaultTheme } from '@/config'; -import { useRpcQueryClient } from '@/hooks'; import '@interchain-ui/react/styles'; function CreateCosmosApp({ Component, pageProps }: AppProps) { const signerOptions: SignerOptions = { + //@ts-ignore signingStargate: (chain: Chain): SigningStargateClientOptions | undefined => { - const mergedRegistry = new Registry([...cosmosProtoRegistry, ...quicksilverProtoRegistry]); + const mergedRegistry = new Registry([...cosmosProtoRegistry, ...quicksilverProtoRegistry, ...ibcProtoRegistry]); const mergedAminoTypes = new AminoTypes({ ...cosmosAminoConverters, ...quicksilverAminoConverters, + ...ibcAminoConverters, }); return { @@ -48,9 +50,194 @@ function CreateCosmosApp({ Component, pageProps }: AppProps) { }, }); + const env = process.env.NEXT_PUBLIC_CHAIN_ENV; + + const rpcEndpoints = { + quicksilver: + env === 'testnet' + ? process.env.NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_QUICKSILVER + : process.env.NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_QUICKSILVER, + cosmoshub: + env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_COSMOSHUB : process.env.NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_COSMOSHUB, + sommelier: + env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_SOMMELIER : process.env.NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_SOMMELIER, + stargaze: + env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_STARGAZE : process.env.NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_STARGAZE, + regen: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_REGEN : process.env.NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_REGEN, + osmosis: + env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_OSMOSIS : process.env.NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_OSMOSIS, + }; + + const lcdEndpoints = { + 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, + }; + + const modalThemeOverrides: ThemeCustomizationProps = { + overrides: { + 'connect-modal': { + bg: { + light: 'rgba(0,0,0,0.75)', + dark: 'rgba(32,32,32,0.9)', + }, + activeBg: { + light: 'rgba(0,0,0,0.75)', + dark: 'rgba(32,32,32,0.9)', + }, + color: { + light: '#FFFFFF', + dark: '#FFFFFF', + }, + }, + 'clipboard-copy-text': { + bg: { + light: '#FFFFFF', + dark: '#FFFFFF', + }, + }, + 'connect-modal-qr-code-shadow': { + bg: { + light: '#FFFFFF', + dark: '#FFFFFF', + }, + }, + button: { + bg: { + light: '#FF8000', + dark: '#FF8000', + }, + }, + 'connect-modal-head-title': { + bg: { + light: '#FFFFFF', + dark: '#FFFFFF', + }, + }, + 'connect-modal-wallet-button-label': { + bg: { + light: '#FFFFFF', + dark: '#FFFFFF', + }, + }, + 'connect-modal-wallet-button-sublogo': { + bg: { + light: '#FFFFFF', + dark: '#FFFFFF', + }, + }, + 'connect-modal-qr-code-loading': { + bg: { + light: '#FFFFFF', + dark: '#FFFFFF', + }, + }, + 'connect-modal-wallet-button': { + bg: { + light: 'rgba(55,55,55,0.9)', + dark: 'rgba(55,55,55,0.9', + }, + hoverBg: { + light: '#FF8000', + dark: '#FF8000', + }, + hoverBorderColor: { + light: 'black', + dark: 'black', + }, + activeBorderColor: { + light: '#FFFFFF', + dark: '#FFFFFF', + }, + color: { + light: '#000000', + dark: '#FFFFFF', + }, + }, + 'connect-modal-qr-code': { + bg: { + light: '', + dark: 'blue', + }, + color: { + light: '#000000', + dark: '#000000', + }, + }, + 'connect-modal-install-button': { + bg: { + light: '#F0F0F0', // Example background color for light theme + dark: '#FF8000', // Example background color for dark theme + }, + // Other properties for 'connect-modal-install-button' if needed + }, + 'connect-modal-qr-code-error': { + bg: { + light: '#FFEEEE', // Example background color for light theme + dark: '#FFFFFF', // Example background color for dark theme + }, + // Other properties for 'connect-modal-qr-code-error' if needed + }, + 'connect-modal-qr-code-error-button': { + bg: { + light: '#FFCCCC', // Example background color for light theme + dark: '#552222', // Example background color for dark theme + }, + }, + }, + }; + return ( - +
    diff --git a/web-ui/pages/airdrop.tsx b/web-ui/pages/airdrop.tsx new file mode 100644 index 000000000..cd7e14063 --- /dev/null +++ b/web-ui/pages/airdrop.tsx @@ -0,0 +1,34 @@ +import AirdropSection from '@/components/Airdrop/airdropSection'; +import DefiTable from '@/components/Defi/defiBox'; +import { ExternalLinkIcon } from '@chakra-ui/icons'; +import { Box, Button, Container, Flex, Stack, Table, Tbody, Td, Th, Thead, Tr, Text, SlideFade } from '@chakra-ui/react'; +import Head from 'next/head'; + +export default function Home() { + return ( + <> + + + + Airdrop + + + + + Airdrop + + + + + + ); +} diff --git a/web-ui/pages/assets.tsx b/web-ui/pages/assets.tsx index cff5f41cc..b4b4bd323 100644 --- a/web-ui/pages/assets.tsx +++ b/web-ui/pages/assets.tsx @@ -10,8 +10,8 @@ import StakingIntent from '@/components/Assets/intents'; import MyPortfolio from '@/components/Assets/portfolio'; import QuickBox from '@/components/Assets/quickbox'; import UnbondingAssetsTable from '@/components/Assets/unbondingTable'; -import { useIntentQuery, useQBalanceQuery, useTokenPriceQuery, useZoneQuery } from '@/hooks/useQueries'; -import { shiftDigits } from '@/utils'; +import { useAPYQuery, useBalanceQuery, useIntentQuery, useQBalanceQuery, useTokenPriceQuery, useZoneQuery } from '@/hooks/useQueries'; +import { shiftDigits, toNumber } from '@/utils'; export interface PortfolioItemInterface { title: string; @@ -29,32 +29,61 @@ type BalanceRates = { [key: string]: string; }; -function Home() { - const { address } = useChain('quicksilver'); - const { address: qAddress, isWalletConnected } = useChain('quicksilver'); +type APYRates = { + [key: string]: Number; +}; - // Define a function to fetch token price data +function Home() { + const { address, isWalletConnected } = useChain('quicksilver'); + // Function to fetch token price from an API const fetchTokenPrice = async (token: any) => { try { const response = await axios.get(`https://api-osmosis.imperator.co/tokens/v2/price/${token}`); - return response.data.price; // Adjust this according to your API response structure + return response.data.price; } catch (error) { console.error('Error fetching token price:', error); return null; } }; - const { balance: qAtom, isLoading: qAtomIsLoading, isError: qAtomIsError } = useQBalanceQuery('quicksilver', qAddress ?? '', 'atom'); - const { balance: qOsmo, isLoading: qOsmoIsLoading, isError: qOsmoIsError } = useQBalanceQuery('quicksilver', qAddress ?? '', 'osmo'); - const { balance: qStars, isLoading: qStarsIsLoading, isError: qStarsIsError } = useQBalanceQuery('quicksilver', qAddress ?? '', 'stars'); - const { balance: qRegen, isLoading: qRegenIsLoading, isError: qRegenIsError } = useQBalanceQuery('quicksilver', qAddress ?? '', 'regen'); - const { balance: qSomm, isLoading: qSommIsLoading, isError: qSommIsError } = useQBalanceQuery('quicksilver', qAddress ?? '', 'somm'); - - const { data: CosmosZone, isLoading: isCosmosZoneLoading, isError: isCosmosZoneError } = useZoneQuery('cosmoshub-4'); - const { data: OsmoZone, isLoading: isOsmoZoneLoading, isError: isOsmoZoneError } = useZoneQuery('osmosis-1'); - const { data: StarZone, isLoading: isStarZoneLoading, isError: isStarZoneError } = useZoneQuery('stargaze-1'); - const { data: RegenZone, isLoading: isRegenZoneLoading, isError: isRegenZoneError } = useZoneQuery('regen-1'); - const { data: SommZone, isLoading: isSommZoneLoading, isError: isSommZoneError } = useZoneQuery('sommelier-3'); + 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; + + // Retrieve balance for each token + const { balance: qAtom, isLoading: qAtomIsLoading, isError: qAtomIsError } = useQBalanceQuery('quicksilver', address ?? '', 'atom'); + const { balance: qOsmo, isLoading: qOsmoIsLoading, isError: qOsmoIsError } = useQBalanceQuery('quicksilver', address ?? '', 'osmo'); + const { balance: qStars, isLoading: qStarsIsLoading, isError: qStarsIsError } = useQBalanceQuery('quicksilver', address ?? '', 'stars'); + const { balance: qRegen, isLoading: qRegenIsLoading, isError: qRegenIsError } = useQBalanceQuery('quicksilver', address ?? '', 'regen'); + const { balance: qSomm, isLoading: qSommIsLoading, isError: qSommIsError } = useQBalanceQuery('quicksilver', address ?? '', 'somm'); + + // Retrieve zone data for each token + const { data: CosmosZone, isLoading: isCosmosZoneLoading, isError: isCosmosZoneError } = useZoneQuery(COSMOSHUB_CHAIN_ID ?? ''); + const { data: OsmoZone, isLoading: isOsmoZoneLoading, isError: isOsmoZoneError } = useZoneQuery(OSMOSIS_CHAIN_ID ?? ''); + const { data: StarZone, isLoading: isStarZoneLoading, isError: isStarZoneError } = useZoneQuery(STARGAZE_CHAIN_ID ?? ''); + const { data: RegenZone, isLoading: isRegenZoneLoading, isError: isRegenZoneError } = useZoneQuery(REGEN_CHAIN_ID ?? ''); + const { data: SommZone, isLoading: isSommZoneLoading, isError: isSommZoneError } = useZoneQuery(SOMMELIER_CHAIN_ID ?? ''); + // Retrieve APY data for each token + const { APY: cosmosAPY, isLoading: isCosmosAPYLoading, isError: isCosmosAPYError } = useAPYQuery('cosmoshub-4'); + const { APY: osmoAPY, isLoading: isOsmoAPYLoading, isError: isOsmoAPYError } = useAPYQuery('osmosis-1'); + const { APY: starsAPY, isLoading: isStarsAPYLoading, isError: isStarsAPYError } = useAPYQuery('stargaze-1'); + const { APY: regenAPY, isLoading: isRegenAPYLoading, isError: isRegenAPYError } = useAPYQuery('regen-1'); + const { APY: sommAPY, isLoading: isSommAPYLoading, isError: isSommAPYError } = useAPYQuery('sommelier-3'); + + // useMemo hook to cache APY data + const qAPYRates: APYRates = useMemo( + () => ({ + qAtom: cosmosAPY, + qOsmo: osmoAPY, + qStars: starsAPY, + qRegen: regenAPY, + qSomm: sommAPY, + }), + [cosmosAPY, osmoAPY, starsAPY, regenAPY, sommAPY], + ); + // useMemo hook to cache qBalance data const qBalances: BalanceRates = useMemo( () => ({ qAtom: shiftDigits(qAtom?.balance.amount ?? '', -6), @@ -66,9 +95,7 @@ function Home() { [qAtom, qOsmo, qStars, qRegen, qSomm], ); - const [portfolioItems, setPortfolioItems] = useState([]); - const [totalPortfolioValue, setTotalPortfolioValue] = useState(0); - + // useMemo hook to cache redemption rate data const redemptionRates: NumericRedemptionRates = useMemo( () => ({ atom: CosmosZone?.redemptionRate ? parseFloat(CosmosZone.redemptionRate) : 1, @@ -80,19 +107,32 @@ function Home() { [CosmosZone, OsmoZone, StarZone, RegenZone, SommZone], ); + // State hooks for portfolio items, total portfolio value, and other metrics + const [portfolioItems, setPortfolioItems] = useState([]); + const [totalPortfolioValue, setTotalPortfolioValue] = useState(0); + const [averageApy, setAverageAPY] = useState(0); + const [totalYearlyYield, setTotalYearlyYield] = useState(0); + // useEffect hook to compute portfolio metrics when dependencies change useEffect(() => { const updatePortfolioItems = async () => { 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(); const price = await fetchTokenPrice(baseToken); const qTokenPrice = price * Number(redemptionRates[baseToken]); 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(), @@ -102,79 +142,134 @@ function Home() { qTokenPrice: qTokenPrice || 0, }); } - - // Now, calculate the percentage of each item - updatedItems = updatedItems.map((item) => ({ - ...item, - percentage: ((((Number(item.amount) * item.qTokenPrice) / totalValue) * 100) / 100).toFixed(2), - })); - + // 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), + }; + }); + // Update state with calculated data setPortfolioItems(updatedItems); setTotalPortfolioValue(totalValue); + setAverageAPY(weightedAPY); + setTotalYearlyYield(totalYearlyYield); }; updatePortfolioItems(); - }, [qBalances, CosmosZone, OsmoZone, StarZone, RegenZone, SommZone, redemptionRates]); + }, [qBalances, CosmosZone, OsmoZone, StarZone, RegenZone, SommZone, redemptionRates, qAPYRates]); + + // useMemo hook to prepare assets data for the AssetsGrid component + const assetsData = useMemo(() => { + return Object.keys(qBalances).map((token) => { + return { + name: token.toUpperCase().replace('Q', 'q'), + balance: toNumber(qBalances[token], 2).toString(), + apy: parseFloat(qAPYRates[token]?.toFixed(2)) || 0, + native: token.replace('q', '').toUpperCase(), + }; + }); + }, [qBalances, qAPYRates]); return ( <> - + Assets - + + + Assets + - {/* Quick box */} - - - - {/* Portfolio box */} - - - - {/* Intent box */} - - - + {!isWalletConnected && ( + + + Wallet is not connected! Please connect your wallet to view the Assets Section. + + + )} + {isWalletConnected && ( + <> + + + + + + + + + + + + )} {/* Assets Grid */} - + {/* Unbonding Table */} - + @@ -182,10 +277,11 @@ function Home() { ); } - +// disable ssr in order to use useQuery hooks const DynamicAssetsPage = dynamic(() => Promise.resolve(Home), { ssr: false, }); + const AssetsWrapper = () => { return ; }; diff --git a/web-ui/pages/defi.tsx b/web-ui/pages/defi.tsx index fb5e4a8a0..4292d1232 100644 --- a/web-ui/pages/defi.tsx +++ b/web-ui/pages/defi.tsx @@ -1,16 +1,33 @@ -import { Box, Container } from '@chakra-ui/react'; +import DefiTable from '@/components/Defi/defiBox'; +import { ExternalLinkIcon } from '@chakra-ui/icons'; +import { Box, Button, Container, Flex, Stack, Table, Tbody, Td, Th, Thead, Tr, Text, SlideFade } from '@chakra-ui/react'; import Head from 'next/head'; export default function Home() { return ( <> - - - DeFi - - - - + + + + DeFi + + + + + DeFi Opportunities + + + + ); } diff --git a/web-ui/pages/governance.tsx b/web-ui/pages/governance.tsx index 4d0169c7e..40daead63 100644 --- a/web-ui/pages/governance.tsx +++ b/web-ui/pages/governance.tsx @@ -14,30 +14,19 @@ export default function Home() { return ( <> - - - - Governance - - - - - - Proposals - - {chainName && } - - - + + + Governance + + + + + + Proposals + + {chainName && } + + ); diff --git a/web-ui/pages/index.tsx b/web-ui/pages/index.tsx deleted file mode 100644 index a47cdc3f4..000000000 --- a/web-ui/pages/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Box, Flex } from '@chakra-ui/react'; -import Head from 'next/head'; - -import { Header } from '@/components'; -import { SideHeader } from '@/components'; -import LiquidMetalSphere from '@/components/ThreeJS/liquidMetalSphere'; - -export default function Home() { - return ( - <> - - - Quicksilver - - - - - - - - - - ); -} diff --git a/web-ui/pages/staking/[chainId]/[valoperAddress].tsx b/web-ui/pages/staking/[chainId]/[valoperAddress].tsx new file mode 100644 index 000000000..3b82535f8 --- /dev/null +++ b/web-ui/pages/staking/[chainId]/[valoperAddress].tsx @@ -0,0 +1,9 @@ +import { Text } from '@interchain-ui/react'; + +export default function Home() { + return ( + <> + Hello world + + ); +} diff --git a/web-ui/pages/staking.tsx b/web-ui/pages/staking/index.tsx similarity index 80% rename from web-ui/pages/staking.tsx rename to web-ui/pages/staking/index.tsx index d57734602..218422803 100644 --- a/web-ui/pages/staking.tsx +++ b/web-ui/pages/staking/index.tsx @@ -17,14 +17,15 @@ import { } from '@chakra-ui/react'; import dynamic from 'next/dynamic'; import Head from 'next/head'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Header, NetworkSelect, SideHeader } from '@/components'; import { StakingBox } from '@/components'; import { InfoBox } from '@/components'; import { AssetsAccordian } from '@/components'; import { useAPYQuery } from '@/hooks/useQueries'; -import { networks } from '@/state/chains/prod'; +import { networks as prodNetworks, testNetworks as devNetworks } from '@/state/chains/prod'; +import { useChain } from '@cosmos-kit/react'; const DynamicStakingBox = dynamic(() => Promise.resolve(StakingBox), { ssr: false, @@ -38,10 +39,25 @@ const DynamicAssetBox = dynamic(() => Promise.resolve(AssetsAccordian), { ssr: false, }); +const networks = process.env.NEXT_PUBLIC_CHAIN_ENV === 'mainnet' ? prodNetworks : devNetworks; + export default function Staking() { const [selectedNetwork, setSelectedNetwork] = useState(networks[0]); const [isModalOpen, setModalOpen] = useState(false); - const { APY, isLoading, isError } = useAPYQuery(selectedNetwork.chainId); + let newChainId; + if (selectedNetwork.chainId === 'provider') { + newChainId = 'cosmoshub-4'; + } else if (selectedNetwork.chainId === 'elgafar-1') { + newChainId = 'stargaze-1'; + } else if (selectedNetwork.chainId === 'osmo-test-5') { + newChainId = 'osmosis-1'; + } else if (selectedNetwork.chainId === 'regen-redwood-1') { + newChainId = 'regen-1'; + } else { + // Default case + newChainId = selectedNetwork.chainId; + } + const { APY, isLoading, isError } = useAPYQuery(newChainId); const [balance, setBalance] = useState(''); const [qBalance, setQBalance] = useState(''); @@ -59,13 +75,13 @@ export default function Staking() { Staking - + - + {/* {''} + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web-ui/public/img/networks/qregen.png b/web-ui/public/img/networks/qregen.png new file mode 100644 index 0000000000000000000000000000000000000000..a7f5c47345966190b4890a26c9026658881059e5 GIT binary patch literal 28660 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 literal 0 HcmV?d00001 diff --git a/web-ui/public/img/textLogo.svg b/web-ui/public/img/textLogo.svg new file mode 100644 index 000000000..dbb551644 --- /dev/null +++ b/web-ui/public/img/textLogo.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web-ui/services/zone.ts b/web-ui/services/zone.ts index a9953b45b..f8d31591a 100644 --- a/web-ui/services/zone.ts +++ b/web-ui/services/zone.ts @@ -12,109 +12,9 @@ export const statusList = [ ] -export const getZoneWithChainId = async (chainId) => { - try { - 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('Fail to query zones') - } - const zone = zones.filter(z => { - if (z.chain_id === chainId) { - return true - } - return false - }) - if (zone.length === 0) { - throw new Error(`No zone with chain id ${chainId} found`) - } - return zone[0] - } - catch (e) { - throw e - } -} - -export const fetchRedemptionRate = async (chainId) => { - let result = { redemptionRate: 0 }; - try { - let rate = await getRedemptionRate(chainId); - result.redemptionRate = parseFloat(rate); - } catch (e) { - console.log(e.message); - } - return result; -}; - - -export const getValidators = async (chainId) => { - try { - const zone = await getZoneWithChainId(chainId) - return zone.validators - } - catch (e) { - throw e - } -} -export const getNativeValidators = async (rpc, status) => { - try { - const tendermint = await Tendermint34Client.connect(rpc) - const baseQuery = new QueryClient(tendermint) - const extension = setupStakingExtension(baseQuery) - let validators = [] - if (status === 'active') { - let res = await extension.staking.validators("BOND_STATUS_BONDED") - if (!res.validators || res.validators.length === 0) { - throw new Error("0 validators found") - } - validators.push(...res.validators) - while (res.pagination.nextKey.length !== 0) { - res = await extension.staking.validators("BOND_STATUS_BONDED", res.pagination.nextKey) - validators.push(...res.validators) - } - } - else { - let res = await extension.staking.validators("BOND_STATUS_UNBONDING") - if (res.validators) { - validators.push(...res.validators) - while (res.pagination.nextKey.length !== 0) { - res = await extension.staking.validators("BOND_STATUS_UNBONDING", res.pagination.nextKey) - validators.push(...res.validators) - } - } - - res = await extension.staking.validators("BOND_STATUS_UNBONDED") - if (res.validators) { - validators.push(...res.validators) - while (res.pagination.nextKey.length !== 0) { - res = await extension.staking.validators("BOND_STATUS_UNBONDED", res.pagination.nextKey) - validators.push(...res.validators) - } - } - } - return validators - } - catch (e) { - throw e - } -} - -export const getValidatorsFromAPI = async (chainId) => { - try { - const res = await axios.get(`${process.env.NEXT_PUBLIC_QUICKSILVER_DATA_API}/validatorList/${chainId}`) - const { validators } = res.data - if (!validators) { - return [] - } - return validators - } catch (e) { - throw e - } -} - export const getAPY = async (chainId: string) => { try { - const res = await axios.get(`https://data.quicksilver.zone/apr`) + const res = await axios.get(`${process.env.NEXT_PUBLIC_QUICKSILVER_DATA_API}/apr`) const { chains } = res.data if (!chains) { return 0 @@ -128,37 +28,3 @@ export const getAPY = async (chainId: string) => { } } -export const getLogo = (address, chainName) => { - return `https://raw.githubusercontent.com/cosmostation/chainlist/master/chain/${chainName}/moniker/${address}.png` -} - -export const getRedemptionRate = async (chainId) => { - try { - if (chainId) { - const zone = await getZoneWithChainId(chainId) - return zone.redemption_rate - } - return 1 - } - catch (e) { - throw e - } -} - -export const getZoneLocal = (chainId) => { - try { - const zone = ProdZoneInfos.filter(z => { - if (z.chain_id === chainId) { - return true - } - return false - }) - if (zone.length === 0) { - throw new Error(`No zone with chain id ${chainId} found`) - } - return zone[0] - } - catch (e) { - throw e - } -} \ No newline at end of file diff --git a/web-ui/state/chains/prod.ts b/web-ui/state/chains/prod.ts index e377060a1..71cc8685b 100644 --- a/web-ui/state/chains/prod.ts +++ b/web-ui/state/chains/prod.ts @@ -1,46 +1,90 @@ export const networks = [ { value: 'ATOM', - logo: '/quicksilver-app-v2/img/networks/atom.svg', - qlogo: '/quicksilver-app-v2/img/networks/qatom.svg', + logo: '/quicksilver/img/networks/atom.svg', + qlogo: '/quicksilver/img/networks/qatom.svg', name: 'Cosmos Hub', chainName: 'cosmoshub', chainId: 'cosmoshub-4', }, { value: 'OSMO', - logo: '/quicksilver-app-v2/img/networks/osmosis.svg', - qlogo: '/quicksilver-app-v2/img/networks/qosmo.svg', + logo: '/quicksilver/img/networks/osmosis.svg', + qlogo: '/quicksilver/img/networks/qosmo.svg', name: 'Osmosis', chainName: 'osmosis', chainId: 'osmosis-1', }, { value: 'STARS', - logo: '/quicksilver-app-v2/img/networks/stargaze.svg', - qlogo: '/quicksilver-app-v2/img/networks/qstars.svg', + logo: '/quicksilver/img/networks/stargaze.svg', + qlogo: '/quicksilver/img/networks/qstars.svg', name: 'Stargaze', chainName: 'stargaze', chainId: 'stargaze-1', }, { value: 'REGEN', - logo: '/quicksilver-app-v2/img/networks/regen.svg', - qlogo: '/quicksilver-app-v2/img/networks/regen.svg', + logo: '/quicksilver/img/networks/regen.svg', + qlogo: '/quicksilver/img/networks/qregen.png', name: 'Regen', chainName: 'regen', chainId: 'regen-1', }, { value: 'SOMM', - logo: '/quicksilver-app-v2/img/networks/sommelier.png', - qlogo: '/quicksilver-app-v2/img/networks/sommelier.png', + logo: '/quicksilver/img/networks/sommelier.png', + qlogo: '/quicksilver/img/networks/sommelier.png', name: 'Sommelier', chainName: 'sommelier', chainId: 'sommelier-3', }, ]; + export const testNetworks = [ + { + value: 'ATOM', + logo: '/quicksilver/img/networks/atom.svg', + qlogo: '/quicksilver/img/networks/qatom.svg', + name: 'Cosmos Hub', + chainName: 'cosmoshub', + chainId: 'provider', + }, + { + value: 'OSMO', + logo: '/quicksilver/img/networks/osmosis.svg', + qlogo: '/quicksilver/img/networks/qosmo.svg', + name: 'Osmosis', + chainName: 'osmosis', + chainId: 'osmo-test-5', + }, + { + value: 'STARS', + logo: '/quicksilver/img/networks/stargaze.svg', + qlogo: '/quicksilver/img/networks/qstars.svg', + name: 'Stargaze', + chainName: 'stargaze', + chainId: 'elgafar-1', + }, + { + value: 'REGEN', + logo: '/quicksilver/img/networks/regen.svg', + qlogo: '/quicksilver/img/networks/qregen.png', + name: 'Regen', + chainName: 'regen', + chainId: 'regen-redwood-1', + }, + { + value: 'SOMM', + logo: '/quicksilver/img/networks/sommelier.png', + qlogo: '/quicksilver/img/networks/sommelier.png', + name: 'Sommelier', + chainName: 'sommelier', + chainId: 'sommelier-3', + }, + ]; + + export const ProdQuickSilverChainInfo = { chainId: "quicksilver-2", chainName: "Quicksilver Protocol", diff --git a/web-ui/test/Staking.test.tsx b/web-ui/test/Staking.test.tsx index 0f9a2c237..3a0cd9de2 100644 --- a/web-ui/test/Staking.test.tsx +++ b/web-ui/test/Staking.test.tsx @@ -10,40 +10,40 @@ import Staking from '../pages/staking'; const networks = [ { value: 'ATOM', - logo: '/quicksilver-app-v2/img/networks/atom.svg', - qlogo: '/quicksilver-app-v2/img/networks/q-atom.svg', + logo: '/quicksilver/img/networks/atom.svg', + qlogo: '/quicksilver/img/networks/q-atom.svg', name: 'Cosmos Hub', chainName: 'cosmoshub', chainId: 'cosmoshub-4', }, { value: 'OSMO', - logo: '/quicksilver-app-v2/img/networks/osmosis.svg', - qlogo: '/quicksilver-app-v2/img/networks/qosmo.svg', + logo: '/quicksilver/img/networks/osmosis.svg', + qlogo: '/quicksilver/img/networks/qosmo.svg', name: 'Osmosis', chainName: 'osmosis', chainId: 'osmosis-1', }, { value: 'STARS', - logo: '/quicksilver-app-v2/img/networks/stargaze.svg', - qlogo: '/quicksilver-app-v2/img/networks/stargaze-2.png', + logo: '/quicksilver/img/networks/stargaze.svg', + qlogo: '/quicksilver/img/networks/stargaze-2.png', name: 'Stargaze', chainName: 'stargaze', chainId: 'stargaze-1', }, { value: 'REGEN', - logo: '/quicksilver-app-v2/img/networks/regen.svg', - qlogo: '/quicksilver-app-v2/img/networks/regen.svg', + logo: '/quicksilver/img/networks/regen.svg', + qlogo: '/quicksilver/img/networks/regen.svg', name: 'Regen', chainName: 'regen', chainId: 'regen-1', }, { value: 'SOMM', - logo: '/quicksilver-app-v2/img/networks/sommelier.png', - qlogo: '/quicksilver-app-v2/img/networks/sommelier.png', + logo: '/quicksilver/img/networks/sommelier.png', + qlogo: '/quicksilver/img/networks/sommelier.png', name: 'Sommelier', chainName: 'sommelier', chainId: 'sommelier-3', diff --git a/web-ui/tx/ibcTransferTx.tsx b/web-ui/tx/ibcTransferTx.tsx new file mode 100644 index 000000000..aad62fedc --- /dev/null +++ b/web-ui/tx/ibcTransferTx.tsx @@ -0,0 +1,108 @@ +import { Box, Link, useToast, Text } from '@chakra-ui/react'; +import { SigningStargateClient, Coin, StdFee } from '@cosmjs/stargate'; +import { ChainName } from '@cosmos-kit/core'; +import { quicksilver } from 'quicksilverjs'; +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/liquidStakeTx.tsx b/web-ui/tx/liquidStakeTx.tsx index ea30a1e71..ad936ef7a 100644 --- a/web-ui/tx/liquidStakeTx.tsx +++ b/web-ui/tx/liquidStakeTx.tsx @@ -4,8 +4,8 @@ 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 { assets } from '@chalabi/chain-registry'; +import chains from '@chalabi/chain-registry'; import { cosmos } from 'interchain-query'; import { Zone } from 'quicksilverjs/types/codegen/quicksilver/interchainstaking/v1/interchainstaking'; import { SetStateAction } from 'react'; @@ -84,7 +84,7 @@ export const liquidStakeTx = ( if (validatorsSelect.length > 0) { validatorsSelect.forEach((val) => { - memoBuffer = Buffer.concat([memoBuffer, addValidator(val.address, val.intent / 100)]); + memoBuffer = Buffer.concat([memoBuffer, addValidator(val.address, val.intent)]); }); memoBuffer = Buffer.concat([Buffer.from([0x02, memoBuffer.length]), memoBuffer]); } @@ -125,6 +125,7 @@ export const liquidStakeTx = ( try { const response = await stargateClient.signAndBroadcast(address, [msgSend], fee, memo); + setResp(JSON.stringify(response, null, 2)); setIsSigning(false); showSuccessToast(toast, response.transactionHash, chainName); diff --git a/web-ui/utils/ibc.ts b/web-ui/utils/ibc.ts new file mode 100644 index 000000000..ff67e6480 --- /dev/null +++ b/web-ui/utils/ibc.ts @@ -0,0 +1,81 @@ +import { Asset, AssetList } from '@chain-registry/types'; +import { asset_lists as assetLists } from '@chain-registry/assets'; +import { Coin } from 'interchain-query/cosmos/base/v1beta1/coin'; +import { assets, ibc } from 'chain-registry'; +import { shiftDigits } from './maths'; + +export const truncateDenom = (denom: string) => { + return denom.slice(0, 10) + '...' + denom.slice(-6); +}; + + + + + + +const filterAssets = (chainName: string, assetList: AssetList[]): Asset[] => { + return ( + assetList + .find(({ chain_name }) => chain_name === chainName) + ?.assets?.filter(({ type_asset }) => type_asset !== 'ics20') || [] + ); +}; + +const getAllAssets = (chainName: string) => { + const nativeAssets = filterAssets(chainName, assets); + const ibcAssets = filterAssets(chainName, assetLists); + + return [...nativeAssets, ...ibcAssets]; +}; + +export const denomToAsset = (chainName: string, denom: string) => { + return getAllAssets(chainName).find((asset) => asset.base === denom); +}; + +export const denomToExponent = (chainName: string, denom: string) => { + const asset = denomToAsset(chainName, denom); + const unit = asset?.denom_units.find(({ denom }) => denom === asset.display); + return unit?.exponent || 6; +}; + +export const prettyBalance = (chainName: string, balance: Coin) => { + const { amount, denom } = balance; + const asset = denomToAsset(chainName, denom); + const symbol = asset?.symbol || truncateDenom(denom); + const exponent = denomToExponent(chainName, denom); + const displayAmount = shiftDigits(amount, -exponent); + const logoUrl = Object.values(asset?.logo_URIs || {}).find((url) => url); + + return { denom, symbol, amount, displayAmount, logoUrl, exponent }; +}; + +export type PrettyBalance = ReturnType; + +export const getIbcInfo = (fromChainName: string, toChainName: string) => { + let flipped = false; + + let ibcInfo = ibc.find( + (i) => + i.chain_1.chain_name === fromChainName && + i.chain_2.chain_name === toChainName + ); + + if (!ibcInfo) { + ibcInfo = ibc.find( + (i) => + i.chain_1.chain_name === toChainName && + i.chain_2.chain_name === fromChainName + ); + flipped = true; + } + + if (!ibcInfo) { + throw new Error('cannot find IBC info'); + } + + const key = flipped ? 'chain_2' : 'chain_1'; + const sourcePort = ibcInfo.channels[0][key].port_id; + const sourceChannel = ibcInfo.channels[0][key].channel_id; + + return { sourcePort, sourceChannel }; +}; \ No newline at end of file diff --git a/web-ui/utils/index.ts b/web-ui/utils/index.ts index b45cf3ff5..6999de7c0 100644 --- a/web-ui/utils/index.ts +++ b/web-ui/utils/index.ts @@ -2,3 +2,4 @@ export * from './voting'; export * from './staking'; export * from './maths'; export * from './logos'; +export * from './ibc'; \ No newline at end of file diff --git a/web-ui/utils/logos.ts b/web-ui/utils/logos.ts index 56e3e0194..daa2395a9 100644 --- a/web-ui/utils/logos.ts +++ b/web-ui/utils/logos.ts @@ -61,38 +61,38 @@ export const isUrlValid = async (url: string) => { return contentType.startsWith('image'); }; -export const getCosmostationUrl = ( - chainName: string, - validatorAddr: string, -) => { - const cosmostationChainName = - convertChainName(chainName); - return `https://raw.githubusercontent.com/cosmostation/chainlist/main/chain/${cosmostationChainName}/moniker/${validatorAddr}.png`; -}; +export const getCosmostationUrl = (chainName: string, validatorAddr: string) => { + const env = process.env.NEXT_PUBLIC_CHAIN_ENV; + const convertedChainName = convertChainName(chainName); -export const getKeybaseUrl = ( - identity: string, -) => { - return `https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=${identity}&fields=pictures`; + + if (env === 'testnet') { + return 'https://cdn.pixabay.com/photo/2016/08/08/09/17/avatar-1577909_960_720.png'; + } + + const dynamicChainName = `${convertedChainName}${env === 'testnet' ? '-testnet' : ''}`; + return `https://raw.githubusercontent.com/cosmostation/chainlist/main/chain/${dynamicChainName}/moniker/${validatorAddr}.png`; }; -export const addLogoUrlSource = async ( - validator: Validator, - chainName: string, -): Promise => { - const url = getCosmostationUrl( - chainName, - validator.address, - ); - const isValid = await isUrlValid(url); +export const addLogoUrlSource = async (validator: Validator, chainName: string): Promise => { + const url = getCosmostationUrl(chainName, validator.address); + + // If we are not in testnet, check if URL is valid + const isValid = process.env.NEXT_PUBLIC_CHAIN_ENV !== 'testnet' && await isUrlValid(url); + return { ...validator, - imageSource: isValid - ? 'cosmostation' - : 'keybase', + imageSource: isValid ? 'cosmostation' : 'keybase', }; }; + +export const getKeybaseUrl = ( + identity: string, +) => { + return `https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=${identity}&fields=pictures`; +}; + export const getLogoUrls = async ( validators: Validator[], chainName: string, diff --git a/web-ui/utils/staking.ts b/web-ui/utils/staking.ts index 71b94fbeb..b6c912618 100644 --- a/web-ui/utils/staking.ts +++ b/web-ui/utils/staking.ts @@ -4,8 +4,13 @@ import { QueryDelegationTotalRewardsResponse } from 'interchain-query/cosmos/dis 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 { decodeUint8Arr, isGreaterThanZero, shiftDigits, toNumber } from '.'; +import { Any } from 'cosmjs-types/google/protobuf/any'; +import { AnySDKType } from 'interchain-query/google/protobuf/any'; + const DAY_TO_SECONDS = 24 * 60 * 60; const ZERO = '0'; @@ -30,27 +35,58 @@ export const calcStakingApr = ({ pool, commission, communityTax, annualProvision export type ParsedValidator = ReturnType[0]; +function extractValconsPrefix(operatorAddress: string): string { + const prefixEndIndex = operatorAddress.indexOf('valoper'); + const chainPrefix = operatorAddress.substring(0, prefixEndIndex); + return `${chainPrefix}valcons`; +} + + export const parseValidators = (validators: Validator[]) => { return validators.map((validator) => { - const commissionRate = validator.commission?.commission_rates?.rate || ZERO; - - - // If you need to convert to percentage, for example const commissionPercentage = parseFloat(commissionRate) * 100; + const valconsPrefix = extractValconsPrefix(validator.operator_address); + const valconsAddress = getValconsAddress(validator.consensus_pubkey, valconsPrefix); return { + consensusPubkey: validator.consensus_pubkey || '', + valconsAddress, description: validator.description?.details || '', name: validator.description?.moniker || '', identity: validator.description?.identity || '', address: validator.operator_address || '', - commission: commissionPercentage.toFixed() + '%', // Assuming you want to display as percentage + commission: commissionPercentage.toFixed() + '%', votingPower: toNumber(shiftDigits(validator.tokens, -6, 0), 0), }; }); }; +function getValconsAddress(consensus_pubkey: any, valconsPrefix: string) { + if (!consensus_pubkey || typeof consensus_pubkey.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'); + + // Convert bytes to Bech32 words + const valconsWords = bech32.bech32.toWords(new Uint8Array(decoded)); + + // Encode to Bech32 with the given prefix + const valconsAddress = bech32.bech32.encode(valconsPrefix, valconsWords); + + return valconsAddress; + } catch (error) { + console.error('Error in generating valcons address:', error); + return ''; + } +} + + export type ExtendedValidator = ReturnType[0]; export type ChainMetaData = { @@ -76,7 +112,7 @@ export const extendValidators = (validators: ParsedValidator[] = [], chainMetada return { ...validator, - apr, + }; }); }; @@ -128,3 +164,15 @@ export const parseAnnualProvisions = (data: QueryAnnualProvisionsResponse) => { const res = shiftDigits(decodeUint8Arr(data?.annualProvisions), -18); return isGreaterThanZero(res) ? res : null; }; +function wordArrayToUint8Array(wordArray: CryptoJS.lib.WordArray) { + const words = wordArray.words; + const sigBytes = wordArray.sigBytes; + const u8 = new Uint8Array(sigBytes); + + for (let i = 0; i < sigBytes; i++) { + const byte = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + u8[i] = byte; + } + + return u8; +} \ No newline at end of file