├── .editorconfig ├── .env ├── .env.example ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── components ├── ConnectedWalletButton.tsx ├── ErrorBoundary.tsx ├── Input │ ├── LiquidityInput.tsx │ └── index.ts ├── Layout │ ├── AppLayout.tsx │ ├── ExtensionSidebar.tsx │ ├── FooterBar.tsx │ ├── NavigationSidebar.tsx │ ├── PageHeader.tsx │ └── index.ts ├── MigrationCard.tsx ├── NextJsAppRoot.tsx ├── TestnetDialog.tsx ├── TokenSelectList.tsx ├── index.ts └── types.ts ├── features ├── assets │ ├── components │ │ ├── AssetCard.tsx │ │ ├── AssetsList.tsx │ │ ├── DepositRedirectDialog.tsx │ │ ├── TransferDialog │ │ │ ├── AmountInput.tsx │ │ │ ├── AssetSelector.tsx │ │ │ ├── TokenOptionsList.tsx │ │ │ ├── TransferDialog.tsx │ │ │ ├── WalletInfo.tsx │ │ │ ├── index.tsx │ │ │ ├── types.ts │ │ │ └── useTransferAssetMutation.ts │ │ └── index.ts │ ├── hooks │ │ └── useGetSupportedAssetsBalancesOnChain.ts │ └── index.ts ├── liquidity │ ├── components │ │ ├── AdditionalUnderlyingAssetsRow.tsx │ │ ├── AprPill.tsx │ │ ├── BaseCardForEmptyState.tsx │ │ ├── BondLiquidityDialog.tsx │ │ ├── ButtonWithDropdownForSorting.tsx │ │ ├── LiquidityBreakdown.tsx │ │ ├── LiquidityHeader.tsx │ │ ├── LiquidityInputSelector.tsx │ │ ├── LiquidityRewardsCard.tsx │ │ ├── ManageBondedLiquidityCard.tsx │ │ ├── ManageLiquidityCard.tsx │ │ ├── ManagePoolDialog │ │ │ ├── ManagePoolDialog.tsx │ │ │ ├── TokenToTokenRates.tsx │ │ │ ├── index.ts │ │ │ └── usePoolDialogController.tsx │ │ ├── PercentageSelection.tsx │ │ ├── PoolCard.tsx │ │ ├── SegmentedRewardsSimulator.tsx │ │ ├── StakingSummary.tsx │ │ ├── StateSwitchButtons.tsx │ │ ├── StepIcon.tsx │ │ ├── UnbondingLiquidityCard.tsx │ │ ├── UnbondingLiquidityStatusList.tsx │ │ └── UnderlyingAssetRow.tsx │ ├── hooks │ │ ├── index.ts │ │ ├── useClaimTokens.ts │ │ ├── usePoolPairTokenAmount.ts │ │ ├── usePoolTokensDollarValue.ts │ │ ├── useSortPools.ts │ │ ├── useStakedTokenBalance.ts │ │ ├── useStakingClaims.ts │ │ └── useUnstakingDuration.ts │ └── index.ts └── swap │ ├── components │ ├── ConvenienceBalanceButtons.tsx │ ├── QueryInput.tsx │ ├── SelectorInput.tsx │ ├── SelectorToggle.tsx │ ├── SlippageSelector.tsx │ ├── TokenOptionsList.tsx │ ├── TokenSelector.tsx │ ├── TokenSwapModule.tsx │ ├── TransactionAction.tsx │ ├── TransactionTips.tsx │ └── index.ts │ ├── hooks │ ├── index.ts │ ├── usePriceForOneToken.ts │ ├── useSwapFee.ts │ ├── useTokenSwap.tsx │ ├── useTokenToTokenPrice.tsx │ └── useTxRates.ts │ ├── index.ts │ └── swapAtoms.ts ├── hooks ├── useBondTokens.ts ├── useChainInfo.ts ├── useConnectIBCWallet.ts ├── useConnectWallet.ts ├── useCosmWasmClient.ts ├── useIBCAssetInfo.ts ├── useIBCTokenBalance.tsx ├── useIbcAssetList.ts ├── useQueriesDataSelector.ts ├── useRefetchQueries.ts ├── useRewardsQueries.ts ├── useSwapInfo.ts ├── useTokenBalance.tsx ├── useTokenDollarValue.tsx ├── useTokenInfo.ts ├── useTokenList.ts └── useWalletConnectionStatus.ts ├── icons ├── Analytics.tsx ├── Dollar.tsx ├── Logo.tsx ├── LogoBrighter.tsx ├── LogoText.tsx ├── Pools.tsx ├── PriceData.tsx ├── Swap.tsx ├── Transfer.tsx └── index.ts ├── jest.config.js ├── jest.setup.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── index.tsx ├── pools │ ├── [pool].tsx │ └── index.tsx └── transfer │ └── index.tsx ├── public ├── bg@1x.png ├── bg@2x.png ├── chain_info.local.json ├── chain_info.testnet.json ├── crab.png ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── site.webmanifest ├── fonts │ ├── Inter │ │ ├── Inter-Black.woff │ │ ├── Inter-Black.woff2 │ │ ├── Inter-BlackItalic.woff │ │ ├── Inter-BlackItalic.woff2 │ │ ├── Inter-Bold.woff │ │ ├── Inter-Bold.woff2 │ │ ├── Inter-BoldItalic.woff │ │ ├── Inter-BoldItalic.woff2 │ │ ├── Inter-ExtraBold.woff │ │ ├── Inter-ExtraBold.woff2 │ │ ├── Inter-ExtraBoldItalic.woff │ │ ├── Inter-ExtraBoldItalic.woff2 │ │ ├── Inter-ExtraLight.woff │ │ ├── Inter-ExtraLight.woff2 │ │ ├── Inter-ExtraLightItalic.woff │ │ ├── Inter-ExtraLightItalic.woff2 │ │ ├── Inter-Italic.woff │ │ ├── Inter-Italic.woff2 │ │ ├── Inter-Light.woff │ │ ├── Inter-Light.woff2 │ │ ├── Inter-LightItalic.woff │ │ ├── Inter-LightItalic.woff2 │ │ ├── Inter-Medium.woff │ │ ├── Inter-Medium.woff2 │ │ ├── Inter-MediumItalic.woff │ │ ├── Inter-MediumItalic.woff2 │ │ ├── Inter-Regular.woff │ │ ├── Inter-Regular.woff2 │ │ ├── Inter-SemiBold.woff │ │ ├── Inter-SemiBold.woff2 │ │ ├── Inter-SemiBoldItalic.woff │ │ ├── Inter-SemiBoldItalic.woff2 │ │ ├── Inter-Thin.woff │ │ ├── Inter-Thin.woff2 │ │ ├── Inter-ThinItalic.woff │ │ ├── Inter-ThinItalic.woff2 │ │ ├── Inter-italic.var.woff2 │ │ └── Inter-roman.var.woff2 │ └── JetBrainsMono │ │ ├── JetBrainsMono-Bold.woff2 │ │ ├── JetBrainsMono-BoldItalic.woff2 │ │ ├── JetBrainsMono-ExtraBold.woff2 │ │ ├── JetBrainsMono-ExtraBoldItalic.woff2 │ │ ├── JetBrainsMono-ExtraLight.woff2 │ │ ├── JetBrainsMono-ExtraLightItalic.woff2 │ │ ├── JetBrainsMono-Italic.woff2 │ │ ├── JetBrainsMono-Light.woff2 │ │ ├── JetBrainsMono-LightItalic.woff2 │ │ ├── JetBrainsMono-Medium.woff2 │ │ ├── JetBrainsMono-MediumItalic.woff2 │ │ ├── JetBrainsMono-Regular.woff2 │ │ ├── JetBrainsMono-SemiBold.woff2 │ │ ├── JetBrainsMono-SemiBoldItalic.woff2 │ │ ├── JetBrainsMono-Thin.woff2 │ │ └── JetBrainsMono-ThinItalic.woff2 ├── ibc_assets.json ├── img │ └── keplr-icon.png ├── junoswap.png ├── pools_list.local.json ├── pools_list.testnet.json ├── spinner.svg ├── spring-left.png ├── spring-right.png ├── spring.gltf ├── springs-bg.png ├── switch.svg └── vercel.svg ├── queries ├── queryMyLiquidity.ts ├── queryRewardsContracts.ts ├── queryStakedLiquidity.ts ├── querySwapInfo.ts ├── tokenDollarValueQuery │ ├── fetchDollarPriceByTokenIds.ts │ ├── index.ts │ ├── pricingServiceIsDownAlert.tsx │ └── tokenDollarValueQuery.tsx ├── tokenToTokenPriceQuery.ts ├── types.ts ├── useGetTokenDollarValueQuery.ts ├── usePoolsListQuery.ts ├── useQueryMatchingPoolForSwap │ ├── index.ts │ ├── types.ts │ ├── useQueryMatchingPoolsForSwap.ts │ └── util │ │ ├── selectEligiblePoolsForTokenToTokenSwap.spec.ts │ │ ├── selectEligiblePoolsForTokenToTokenSwap.ts │ │ └── validateIfPassThroughPoolMatchIsUnique.ts ├── useQueryPoolUnstakingDuration.ts └── useQueryPools.ts ├── services ├── cw20.tsx ├── liquidity │ ├── executeAddLiquidity.ts │ ├── executeRemoveLiquidity.ts │ ├── index.ts │ └── queryLiquidityBalance.ts ├── queryClient.ts ├── rewards.ts ├── staking.ts └── swap │ ├── directTokenSwap.ts │ ├── index.ts │ ├── passThroughTokenSwap.ts │ └── price.ts ├── state └── atoms │ ├── transactionAtoms.ts │ └── walletAtoms.ts ├── styles ├── globals.scss ├── inter.font.scss └── jetbrains.font.scss ├── tsconfig.json ├── types └── window.d.ts ├── util ├── constants.ts ├── conversion │ ├── conversion.ts │ ├── index.ts │ └── pools.ts ├── cosmWasmClientRouter.ts ├── externalLinkPopup.ts ├── formatCompactNumber.ts ├── formatSdkErrorMessage.ts ├── getPropsForInteractiveElement.ts ├── localStorageEffect.ts ├── maybePluralize.ts ├── messages │ ├── createExecuteMessage.ts │ ├── createIncreaseAllowanceMessage.ts │ ├── index.ts │ └── validateTransactionSuccess.ts └── testutils.ts ├── webpack.functions.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{ts,tsx,json,js,jsx}] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.md] 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | NEXT_PUBLIC_SITE_TITLE=Wasmswap 4 | NEXT_PUBLIC_TEST_MODE_DISABLED=false 5 | NEXT_PUBLIC_APP_VERSION=1.4.1 6 | NEXT_PUBLIC_GAS_PRICE=0.0025ujunox 7 | 8 | # Asset Information 9 | 10 | NEXT_PUBLIC_CHAIN_INFO_URL=/chain_info.testnet.json 11 | NEXT_PUBLIC_POOLS_LIST_URL=/pools_list.testnet.json 12 | NEXT_PUBLIC_IBC_ASSETS_URL=/ibc_assets.json 13 | 14 | # Feature flags 15 | 16 | NEXT_PUBLIC_ENABLE_FEATURE_TRANSFERS=true 17 | NEXT_PUBLIC_ENABLE_FEATURE_STAKING=true 18 | NEXT_PUBLIC_ENABLE_FEATURE_REWARDS=true 19 | NEXT_PUBLIC_DARK_MODE_ENABLED_BY_DEFAULT=false 20 | 21 | # Contact Information 22 | 23 | NEXT_PUBLIC_FEEDBACK_LINK=# 24 | NEXT_PUBLIC_INTERFACE_GITHUB_LINK=https://github.com/Wasmswap/wasmswap-interface 25 | NEXT_PUBLIC_CONTRACTS_GITHUB_LINK=https://github.com/ben2x4/crustacean-swap 26 | NEXT_PUBLIC_TELEGRAM_LINK=# 27 | NEXT_PUBLIC_DISCORD_LINK=# 28 | NEXT_PUBLIC_TWITTER_LINK=# 29 | 30 | # Governance link 31 | 32 | NEXT_PUBLIC_GOVERNANCE_LINK_URL=# 33 | NEXT_PUBLIC_GOVERNANCE_LINK_LABEL=Governance 34 | 35 | NEXT_PUBLIC_PRICE_LINK_URL=# 36 | NEXT_PUBLIC_PRICE_LINK_LABEL=Price Data 37 | 38 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Basic 2 | NEXT_PUBLIC_SITE_TITLE=Wasmswap 3 | NEXT_PUBLIC_TEST_MODE_DISABLED=false 4 | NEXT_PUBLIC_APP_VERSION=1.0.0 5 | NEXT_PUBLIC_GAS_PRICE=0.0025ujuno 6 | 7 | # Asset Information 8 | NEXT_PUBLIC_CHAIN_INFO_URL=/chain_info.local.json 9 | NEXT_PUBLIC_POOLS_LIST_URL=/pools_list.local.json 10 | NEXT_PUBLIC_IBC_ASSETS_URL=/ibc_assets.local.json 11 | 12 | # Feature flags 13 | NEXT_PUBLIC_ENABLE_FEATURE_TRANSFERS=false 14 | NEXT_PUBLIC_ENABLE_FEATURE_STAKING=false 15 | NEXT_PUBLIC_ENABLE_FEATURE_REWARDS=false 16 | NEXT_PUBLIC_DARK_MODE_ENABLED_BY_DEFAULT=false 17 | 18 | # Contact Information 19 | NEXT_PUBLIC_FEEDBACK_LINK=# 20 | NEXT_PUBLIC_INTERFACE_GITHUB_LINK=https://github.com/Wasmswap/wasmswap-interface 21 | NEXT_PUBLIC_CONTRACTS_GITHUB_LINK=https://github.com/ben2x4/crustacean-swap 22 | NEXT_PUBLIC_TELEGRAM_LINK=# 23 | NEXT_PUBLIC_DISCORD_LINK=# 24 | NEXT_PUBLIC_TWITTER_LINK=# 25 | 26 | # Governance link 27 | NEXT_PUBLIC_GOVERNANCE_LINK_URL=# 28 | NEXT_PUBLIC_GOVERNANCE_LINK_LABEL=Governance 29 | 30 | NEXT_PUBLIC_PRICE_LINK_URL=# 31 | NEXT_PUBLIC_PRICE_LINK_LABEL=Price Data 32 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"], 3 | "plugins": ["testing-library"], 4 | "rules": { 5 | "import/no-duplicates": ["error"] 6 | }, 7 | "overrides": [ 8 | // Only uses Testing Library lint rules in test files 9 | { 10 | "files": [ 11 | "**/__tests__/**/*.[jt]s?(x)", 12 | "**/?(*.)+(spec|test).[jt]s?(x)" 13 | ], 14 | "extends": ["plugin:testing-library/react"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | .idea 22 | *.client-test.* 23 | .yalc 24 | yalc.lock 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env.local 33 | .env.development.local 34 | .env.test.local 35 | .env.production.local 36 | 37 | # vercel 38 | .vercel 39 | 40 | # emacs 41 | *~ 42 | \#*\# 43 | /.emacs.desktop 44 | /.emacs.desktop.lock 45 | *.elc 46 | auto-save-list 47 | tramp 48 | .\#* 49 | .log 50 | 51 | # vim 52 | # swap 53 | [._]*.s[a-v][a-z] 54 | [._]*.sw[a-p] 55 | [._]s[a-v][a-z] 56 | [._]sw[a-p] 57 | # session 58 | Session.vim 59 | # temporary 60 | .netrwhist 61 | *~ 62 | # auto-generated tag files 63 | tags 64 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/.next/** 3 | **/_next/** 4 | **/dist/** 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export class ErrorBoundary extends React.Component<{}, { hasError: boolean }> { 4 | constructor(props) { 5 | super(props) 6 | this.state = { hasError: false } 7 | } 8 | 9 | static getDerivedStateFromError() { 10 | // Update state so the next render will show the fallback UI. 11 | return { hasError: true } 12 | } 13 | 14 | componentDidCatch(error, errorInfo) { 15 | // You can also log the error to an error reporting service 16 | console.error(error) 17 | console.log(errorInfo) 18 | } 19 | 20 | render() { 21 | if (this.state.hasError) { 22 | // You can render any custom fallback UI 23 | return

Something went wrong.

24 | } 25 | 26 | return this.props.children 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /components/Input/LiquidityInput.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BasicNumberInput, 3 | formatTokenBalance, 4 | ImageForTokenLogo, 5 | styled, 6 | Text, 7 | } from 'junoblocks' 8 | import React, { FC, useRef, useState } from 'react' 9 | 10 | import { useTokenInfo } from '../../hooks/useTokenInfo' 11 | 12 | type LiquidityInputProps = { 13 | tokenSymbol: string 14 | availableAmount: number 15 | maxApplicableAmount: number 16 | amount: number 17 | onAmountChange: (value: number) => void 18 | } 19 | 20 | export const LiquidityInput: FC = ({ 21 | tokenSymbol, 22 | availableAmount, 23 | maxApplicableAmount, 24 | amount, 25 | onAmountChange, 26 | }) => { 27 | const [focusedOnInput, setFocusedOnInput] = useState(false) 28 | const inputRef = useRef() 29 | 30 | const { name: tokenName, logoURI } = useTokenInfo(tokenSymbol) 31 | 32 | const handleAmountChange = (value: number) => onAmountChange(value) 33 | 34 | return ( 35 | { 37 | inputRef.current.focus() 38 | }} 39 | active={focusedOnInput} 40 | > 41 | 42 | 43 |
44 | 45 | {tokenName} 46 | 47 | 48 | {formatTokenBalance(availableAmount)} available 49 | 50 |
51 |
52 | 53 | 54 | { 61 | setFocusedOnInput(true) 62 | }} 63 | onBlur={() => { 64 | setFocusedOnInput(false) 65 | }} 66 | /> 67 | 68 | 69 |
70 | ) 71 | } 72 | 73 | const StyledDivForWrapper = styled('div', { 74 | display: 'flex', 75 | alignItems: 'center', 76 | justifyContent: 'space-between', 77 | backgroundColor: '$colors$dark10', 78 | borderRadius: '$1', 79 | padding: '$6 $12 $5 $5', 80 | width: '100%', 81 | transition: 'background 0.1s ease-out', 82 | '&:hover': { 83 | backgroundColor: '$colors$dark15', 84 | }, 85 | '&:active': { 86 | backgroundColor: '$colors$dark5', 87 | }, 88 | variants: { 89 | active: { 90 | true: { 91 | backgroundColor: '$colors$dark10 !important', 92 | }, 93 | }, 94 | }, 95 | }) 96 | 97 | const StyledDivForColumn = styled('div', { 98 | variants: { 99 | kind: { 100 | info: { 101 | display: 'flex', 102 | alignItems: 'center', 103 | columnGap: '$space$6', 104 | }, 105 | input: { 106 | textAlign: 'right', 107 | }, 108 | }, 109 | }, 110 | }) 111 | -------------------------------------------------------------------------------- /components/Input/index.ts: -------------------------------------------------------------------------------- 1 | export { LiquidityInput } from './LiquidityInput' 2 | -------------------------------------------------------------------------------- /components/Layout/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import { media, styled, useMedia } from 'junoblocks' 2 | import { APP_MAX_WIDTH, MAIN_PANE_MAX_WIDTH } from 'util/constants' 3 | 4 | import { ExtensionSidebar } from './ExtensionSidebar' 5 | import { FooterBar } from './FooterBar' 6 | import { NavigationSidebar } from './NavigationSidebar' 7 | 8 | export const AppLayout = ({ 9 | navigationSidebar = , 10 | extensionSidebar = , 11 | footerBar = , 12 | children, 13 | }) => { 14 | const isSmallScreen = useMedia('sm') 15 | const isMediumScreen = useMedia('md') 16 | 17 | if (isSmallScreen) { 18 | return ( 19 | 20 | 21 | {navigationSidebar} 22 | 23 |
{children}
24 |
25 | 26 | 27 |
{footerBar}
28 |
29 |
30 | ) 31 | } 32 | 33 | return ( 34 | 35 | {navigationSidebar} 36 | 37 | 38 |
{children}
39 |
40 | 41 | {!isMediumScreen && extensionSidebar} 42 |
43 | ) 44 | } 45 | 46 | const StyledWrapper = styled('div', { 47 | display: 'flex', 48 | flexDirection: 'row', 49 | justifyContent: 'center', 50 | minHeight: '100vh', 51 | backgroundColor: '$backgroundColors$base', 52 | width: APP_MAX_WIDTH, 53 | maxWidth: '100%', 54 | margin: '0 auto', 55 | [media.md]: { 56 | gridTemplateColumns: '15rem 1fr', 57 | }, 58 | }) 59 | 60 | const StyledContainer = styled('div', { 61 | position: 'relative', 62 | display: 'flex', 63 | flexDirection: 'column', 64 | justifyContent: 'space-between', 65 | padding: '0 $12 $12 $12', 66 | '& main': { 67 | margin: '0 auto', 68 | width: '100%', 69 | }, 70 | maxWidth: '100%', 71 | width: MAIN_PANE_MAX_WIDTH, 72 | [media.sm]: {}, 73 | }) 74 | 75 | const StyledWrapperForMobile = styled('div', { 76 | display: 'flex', 77 | flexDirection: 'column', 78 | justifyContent: 'space-between', 79 | minHeight: '100vh', 80 | backgroundColor: '$backgroundColors$base', 81 | }) 82 | 83 | const StyledContainerForMobile = styled('div', { 84 | position: 'relative', 85 | zIndex: '$1', 86 | '& [data-content]': { 87 | margin: '0 auto', 88 | width: '100%', 89 | padding: '0 $12', 90 | }, 91 | }) 92 | -------------------------------------------------------------------------------- /components/Layout/ExtensionSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ErrorIcon, styled, Text, UpRightArrowIcon } from 'junoblocks' 2 | 3 | import { __TEST_MODE__ } from '../../util/constants' 4 | 5 | export const ExtensionSidebar = () => { 6 | return ( 7 | 8 | 9 | 10 | This is a {__TEST_MODE__ ? 'testnet' : 'beta'} version 11 | 12 | 13 | This website is currently{' '} 14 | {__TEST_MODE__ ? 'operates in testnet mode' : 'in beta'}. Please provide 15 | feedback. 16 | 17 | 27 | 28 | 29 | ) 30 | } 31 | 32 | const StyledDivForWrapper = styled('div', { 33 | flexBasis: '16.5rem', 34 | flexGrow: 0, 35 | flexShrink: 0, 36 | zIndex: 1, 37 | position: 'sticky', 38 | borderLeft: '1px solid $borderColors$inactive', 39 | backgroundColor: '$backgroundColors$base', 40 | top: 0, 41 | right: 0, 42 | width: '100%', 43 | height: '100%', 44 | maxHeight: '100vh', 45 | minHeight: '100vh', 46 | padding: '$11 $12', 47 | }) 48 | 49 | const StyledDivForTitleWrapper = styled('div', { 50 | display: 'flex', 51 | alignItems: 'center', 52 | columnGap: '$4', 53 | }) 54 | 55 | const StyledSpringBottom = styled('img', { 56 | position: 'absolute', 57 | right: 0, 58 | bottom: 0, 59 | width: '125%', 60 | maxWidth: '500px', 61 | zIndex: '$1', 62 | userSelect: 'none', 63 | userDrag: 'none', 64 | }) 65 | -------------------------------------------------------------------------------- /components/Layout/FooterBar.tsx: -------------------------------------------------------------------------------- 1 | import { Button, IconWrapper, styled, Text, UpRightArrow } from 'junoblocks' 2 | import { APP_NAME } from 'util/constants' 3 | 4 | export const FooterBar = () => { 5 | return ( 6 | 7 | 8 | {APP_NAME} v{process.env.NEXT_PUBLIC_APP_VERSION} 9 | 10 | 11 | 22 | 23 | 24 | ) 25 | } 26 | 27 | const StyledFooter = styled('footer', { 28 | padding: '$12 0', 29 | alignItems: 'center', 30 | display: 'flex', 31 | }) 32 | 33 | const StyledDivForGrid = styled('div', { 34 | display: 'flex', 35 | alignItems: 'center', 36 | columnGap: '$space$14', 37 | }) 38 | -------------------------------------------------------------------------------- /components/Layout/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Text, useMedia } from 'junoblocks' 2 | import Head from 'next/head' 3 | import React from 'react' 4 | 5 | import { APP_NAME } from '../../util/constants' 6 | 7 | export const PageHeader = ({ title, subtitle }) => { 8 | const isSmall = useMedia('sm') 9 | 10 | return ( 11 | <> 12 | 13 | 14 | {APP_NAME} — {title} 15 | 16 | 17 | 18 | {title} 19 | 20 | 21 | {subtitle} 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /components/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export { AppLayout } from './AppLayout' 2 | export { ExtensionSidebar } from './ExtensionSidebar' 3 | export { FooterBar } from './FooterBar' 4 | export { NavigationSidebar } from './NavigationSidebar' 5 | export { PageHeader } from './PageHeader' 6 | -------------------------------------------------------------------------------- /components/MigrationCard.tsx: -------------------------------------------------------------------------------- 1 | import { styled, Text } from 'junoblocks' 2 | import Link from 'next/link' 3 | 4 | export const MigrationCard = () => { 5 | return ( 6 | 7 | 8 | Junoswap is migrating liquidity to WYND DEX. As part of this transition, 9 | most RAW pools have had swaps and deposits frozen. These pools will be 10 | automatically migrated upon the launch of WYND DEX. Users with liquidity 11 | in these pools may still withdraw funds if they do not want to be 12 | migrated. All other pools are trading as normal and most will also be 13 | automatically migrated. Please see RAW DAO prop 18 for the full details 14 | and list of pools eligible for automatic migration.{' '} 15 | 16 | More details 17 | 18 | 19 | 20 | ) 21 | } 22 | 23 | const StyledCard = styled('div', { 24 | padding: '$8', 25 | marginTop: '22px', 26 | backgroundColor: '$colors$dark10', 27 | borderRadius: '8px', 28 | }) 29 | 30 | const StyledA = styled('a', { 31 | color: '$textColors$brand', 32 | '&:hover': { 33 | textDecoration: 'underline', 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /components/NextJsAppRoot.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | globalCss, 3 | styled, 4 | useSubscribeDefaultAppTheme, 5 | useThemeClassName, 6 | } from 'junoblocks' 7 | import { useEffect, useState } from 'react' 8 | 9 | const applyGlobalStyles = globalCss({ 10 | body: { 11 | backgroundColor: '$backgroundColors$base', 12 | }, 13 | }) 14 | 15 | const useIsRenderingOnServerSide = () => { 16 | const [isRenderingOnServerSide, setIsRenderingOnServerSide] = useState(true) 17 | 18 | useEffect(() => { 19 | setIsRenderingOnServerSide(false) 20 | }, []) 21 | 22 | return isRenderingOnServerSide 23 | } 24 | 25 | export function NextJsAppRoot({ children }) { 26 | const isRenderingOnServerSide = useIsRenderingOnServerSide() 27 | 28 | const themeClassName = useThemeClassName() 29 | useSubscribeDefaultAppTheme() 30 | 31 | /* apply theme class on body also */ 32 | useEffect(() => { 33 | document.body.classList.add(themeClassName) 34 | applyGlobalStyles() 35 | return () => { 36 | document.body.classList.remove(themeClassName) 37 | } 38 | }, [themeClassName]) 39 | 40 | return ( 41 | 47 | {isRenderingOnServerSide ? null : children} 48 | 49 | ) 50 | } 51 | 52 | const StyledContentWrapper = styled('div', { 53 | backgroundColor: '$backgroundColors$base', 54 | }) 55 | -------------------------------------------------------------------------------- /components/TestnetDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Dialog, DialogContent, DialogHeader, Text } from 'junoblocks' 2 | import { useState } from 'react' 3 | 4 | export const TestnetDialog = () => { 5 | const [isShowing, setShowing] = useState(true) 6 | 7 | const requestClose = () => setShowing(false) 8 | 9 | return ( 10 | 11 | 12 | Demo mode warning 13 | 14 | 15 | 16 | This app is currently in beta and operating in demo mode. The app 17 | serves only the presentation and testing purposes. You will not be 18 | able to trade real assets. 19 | 20 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConnectedWalletButton' 2 | export * from './Input' 3 | export * from './Layout' 4 | export * from './TokenSelectList' 5 | -------------------------------------------------------------------------------- /components/types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithRef, ElementType, HTMLProps } from 'react' 2 | 3 | export type RenderAsType = keyof JSX.IntrinsicElements | ElementType 4 | 5 | export type GetRenderAsProps = 6 | T extends JSX.IntrinsicElements 7 | ? HTMLProps 8 | : T extends ElementType 9 | ? ComponentPropsWithRef 10 | : {} 11 | -------------------------------------------------------------------------------- /features/assets/components/DepositRedirectDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Dialog, DialogContent, DialogHeader, Text } from 'junoblocks' 2 | 3 | type DepositRedirectDialogProps = { 4 | isShowing: boolean 5 | onRequestClose: () => void 6 | tokenSymbol: string 7 | href: string 8 | } 9 | 10 | export const DepositRedirectDialog = ({ 11 | isShowing, 12 | onRequestClose, 13 | tokenSymbol, 14 | href, 15 | }: DepositRedirectDialogProps) => { 16 | return ( 17 | 18 | 19 | External asset deposit 20 | 21 | 22 | 23 | You will be redirected to an external service to deposit your{' '} 24 | {tokenSymbol} on the chain. 25 | 26 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /features/assets/components/TransferDialog/AmountInput.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BasicNumberInput, 3 | Button, 4 | ButtonForWrapper, 5 | formatTokenBalance, 6 | styled, 7 | Text, 8 | useTriggerInputFocus, 9 | } from 'junoblocks' 10 | 11 | type AmountInputProps = { 12 | amount: number 13 | maxApplicableAmount: number 14 | onAmountChange: (amount: number) => void 15 | } 16 | 17 | export const AmountInput = ({ 18 | amount, 19 | maxApplicableAmount, 20 | onAmountChange, 21 | }: AmountInputProps) => { 22 | const { isFocused, bind } = useTriggerInputFocus() 23 | 24 | return ( 25 | 30 | 31 | 38 | 45 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | const StyledButtonForWrapper = styled(ButtonForWrapper, { 62 | padding: '$8 $12 $8 $8 !important', 63 | }) 64 | 65 | const StyledDivForButtons = styled('div', { 66 | columnGap: '$space$4', 67 | display: 'flex', 68 | alignItems: 'center', 69 | }) 70 | 71 | const StyledDivForInputWrapper = styled('div', {}) 72 | -------------------------------------------------------------------------------- /features/assets/components/TransferDialog/TokenOptionsList.tsx: -------------------------------------------------------------------------------- 1 | import { TokenSelectList } from 'components' 2 | import { useIBCAssetList } from 'hooks/useIbcAssetList' 3 | 4 | export const TokenOptionsList = ({ 5 | activeTokenSymbol, 6 | onSelect, 7 | fetchingBalanceMode, 8 | ...props 9 | }) => { 10 | const [tokenList] = useIBCAssetList() 11 | return ( 12 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /features/assets/components/TransferDialog/WalletInfo.tsx: -------------------------------------------------------------------------------- 1 | import { CSS } from '@stitches/react' 2 | import { Logo } from 'icons' 3 | import { ConnectIcon, IconWrapper, styled, Text } from 'junoblocks' 4 | import { useRecoilValue } from 'recoil' 5 | import { ibcWalletState, walletState } from 'state/atoms/walletAtoms' 6 | import { APP_NAME } from 'util/constants' 7 | 8 | export const WalletInfo = ({ label, icon, address, css }) => { 9 | return ( 10 | 11 | {icon} 12 |
13 | {label} 14 | 15 | 16 | 17 | {address || "address wasn't identified yet"} 18 | 19 | 20 |
21 |
22 | ) 23 | } 24 | 25 | type WalletInfoProps = { 26 | css?: CSS 27 | depositing?: boolean 28 | } 29 | 30 | export const KeplrWalletInfo = ({ css, depositing }: WalletInfoProps) => { 31 | const { address: ibcWalletAddress } = useRecoilValue(ibcWalletState) 32 | 33 | return ( 34 | } 38 | address={ibcWalletAddress} 39 | /> 40 | ) 41 | } 42 | 43 | export const AppWalletInfo = ({ css, depositing }: WalletInfoProps) => { 44 | const { address: walletAddress } = useRecoilValue(walletState) 45 | 46 | return ( 47 | } />} 51 | address={walletAddress} 52 | /> 53 | ) 54 | } 55 | 56 | const StyledDivForWrapper = styled('div', { 57 | display: 'flex', 58 | alignItems: 'center', 59 | columnGap: '$space$10', 60 | }) 61 | 62 | const StyledDivForAddressRow = styled('div', { 63 | columnGap: '$space$2', 64 | display: 'flex', 65 | alignItems: 'center', 66 | }) 67 | 68 | const StyledImgForIcon = styled('img', { 69 | width: 32, 70 | height: 32, 71 | }) 72 | -------------------------------------------------------------------------------- /features/assets/components/TransferDialog/index.tsx: -------------------------------------------------------------------------------- 1 | export { TransferDialog } from './TransferDialog' 2 | -------------------------------------------------------------------------------- /features/assets/components/TransferDialog/types.ts: -------------------------------------------------------------------------------- 1 | export type TransactionKind = 'deposit' | 'withdraw' 2 | export type TransactionOrigin = 'platform' | 'wallet' 3 | export type TransactionType = 'incoming' | 'outgoing' 4 | -------------------------------------------------------------------------------- /features/assets/components/TransferDialog/useTransferAssetMutation.ts: -------------------------------------------------------------------------------- 1 | import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' 2 | import { 3 | Coin, 4 | DeliverTxResponse, 5 | MsgTransferEncodeObject, 6 | } from '@cosmjs/stargate' 7 | import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx' 8 | import { Height } from 'cosmjs-types/ibc/core/client/v1/client' 9 | import { IBCAssetInfo } from 'hooks/useIbcAssetList' 10 | import Long from 'long' 11 | import { useMutation } from 'react-query' 12 | import { useRecoilValue } from 'recoil' 13 | import { ibcWalletState, walletState } from 'state/atoms/walletAtoms' 14 | import { convertDenomToMicroDenom } from 'util/conversion' 15 | 16 | import { TransactionKind } from './types' 17 | 18 | type UseTransferAssetMutationArgs = { 19 | transactionKind: TransactionKind 20 | tokenAmount: number 21 | tokenInfo: IBCAssetInfo 22 | } & Parameters[2] 23 | 24 | const sendIbcTokens = ( 25 | senderAddress: string, 26 | recipientAddress: string, 27 | transferAmount: Coin, 28 | sourcePort: string, 29 | sourceChannel: string, 30 | timeoutHeight: Height | undefined, 31 | /** timeout in seconds */ 32 | timeoutTimestamp: number | undefined, 33 | memo = '', 34 | client: SigningCosmWasmClient 35 | ): Promise => { 36 | const timeoutTimestampNanoseconds = timeoutTimestamp 37 | ? Long.fromNumber(timeoutTimestamp).multiply(1_000_000_000) 38 | : undefined 39 | const transferMsg: MsgTransferEncodeObject = { 40 | typeUrl: '/ibc.applications.transfer.v1.MsgTransfer', 41 | value: MsgTransfer.fromPartial({ 42 | sourcePort: sourcePort, 43 | sourceChannel: sourceChannel, 44 | sender: senderAddress, 45 | receiver: recipientAddress, 46 | token: transferAmount, 47 | timeoutHeight: timeoutHeight, 48 | timeoutTimestamp: timeoutTimestampNanoseconds, 49 | }), 50 | } 51 | return client.signAndBroadcast(senderAddress, [transferMsg], 'auto', memo) 52 | } 53 | 54 | export const useTransferAssetMutation = ({ 55 | transactionKind, 56 | tokenAmount, 57 | tokenInfo, 58 | ...mutationArgs 59 | }: UseTransferAssetMutationArgs) => { 60 | const { address, client } = useRecoilValue(walletState) 61 | const { address: ibcAddress, client: ibcClient } = 62 | useRecoilValue(ibcWalletState) 63 | 64 | return useMutation(async () => { 65 | const timeout = Math.floor(new Date().getTime() / 1000) + 600 66 | 67 | if (transactionKind == 'deposit') { 68 | return await ibcClient.sendIbcTokens( 69 | ibcAddress, 70 | address, 71 | { 72 | amount: convertDenomToMicroDenom( 73 | tokenAmount, 74 | tokenInfo.decimals 75 | ).toString(), 76 | denom: tokenInfo.denom, 77 | }, 78 | 'transfer', 79 | tokenInfo.channel, 80 | undefined, 81 | timeout, 82 | 'auto' 83 | ) 84 | } 85 | 86 | if (transactionKind == 'withdraw') { 87 | return await sendIbcTokens( 88 | address, 89 | ibcAddress, 90 | { 91 | amount: convertDenomToMicroDenom( 92 | tokenAmount, 93 | tokenInfo.decimals 94 | ).toString(), 95 | denom: tokenInfo.juno_denom, 96 | }, 97 | 'transfer', 98 | tokenInfo.juno_channel, 99 | undefined, 100 | timeout, 101 | '', 102 | client 103 | ) 104 | } 105 | }, mutationArgs) 106 | } 107 | -------------------------------------------------------------------------------- /features/assets/components/index.ts: -------------------------------------------------------------------------------- 1 | export { AssetCard } from './AssetCard' 2 | export { AssetsList } from './AssetsList' 3 | export * from './TransferDialog' 4 | -------------------------------------------------------------------------------- /features/assets/hooks/useGetSupportedAssetsBalancesOnChain.ts: -------------------------------------------------------------------------------- 1 | import { useIBCAssetList } from 'hooks/useIbcAssetList' 2 | import { useMultipleTokenBalance } from 'hooks/useTokenBalance' 3 | import { useMemo } from 'react' 4 | 5 | export const useGetSupportedAssetsBalancesOnChain = () => { 6 | const [ibcAssetList] = useIBCAssetList() 7 | const tokensList = useMemo( 8 | () => ibcAssetList?.tokens.map(({ symbol }) => symbol), 9 | [ibcAssetList] 10 | ) 11 | 12 | const [tokenBalances, loadingBalances] = useMultipleTokenBalance(tokensList) 13 | 14 | const categorizedBalances = useMemo((): [ 15 | typeof tokenBalances, 16 | typeof tokenBalances 17 | ] => { 18 | if (!tokenBalances?.length) { 19 | const fallbackTokensList = 20 | tokensList?.map((tokenSymbol) => ({ 21 | balance: 0, 22 | tokenSymbol, 23 | })) ?? [] 24 | return [[], fallbackTokensList] 25 | } 26 | 27 | const userTokens = [] 28 | const otherTokens = [] 29 | 30 | for (const token of tokenBalances) { 31 | if (token.balance > 0) { 32 | userTokens.push(token) 33 | } else { 34 | otherTokens.push(token) 35 | } 36 | } 37 | 38 | return [userTokens, otherTokens] 39 | }, [tokenBalances, tokensList]) 40 | 41 | return [loadingBalances, categorizedBalances] as const 42 | } 43 | -------------------------------------------------------------------------------- /features/assets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components' 2 | -------------------------------------------------------------------------------- /features/liquidity/components/AdditionalUnderlyingAssetsRow.tsx: -------------------------------------------------------------------------------- 1 | import { useTokenDollarValue } from 'hooks/useTokenDollarValue' 2 | import { 3 | Button, 4 | Column, 5 | dollarValueFormatterWithDecimals, 6 | formatTokenBalance, 7 | ImageForTokenLogo, 8 | InfoIcon, 9 | Inline, 10 | protectAgainstNaN, 11 | Text, 12 | Tooltip, 13 | } from 'junoblocks' 14 | import { useMemo } from 'react' 15 | 16 | import { TokenInfo } from '../../../queries/usePoolsListQuery' 17 | 18 | type AdditionalUnderlyingAssetsRowProps = { 19 | assets: Array<{ 20 | tokenInfo: Pick 21 | tokenAmount: number 22 | }> 23 | } 24 | 25 | export const AdditionalUnderlyingAssetsRow = ({ 26 | assets, 27 | }: AdditionalUnderlyingAssetsRowProps) => { 28 | const assetsBreakdownInfo = useMemo( 29 | () => ( 30 | 31 | {assets.map(({ tokenInfo, tokenAmount }) => ( 32 | 33 | {formatTokenBalance(tokenAmount, { includeCommaSeparation: true })}{' '} 34 | {tokenInfo.symbol} ( 35 | 39 | ) 40 | 41 | ))} 42 | 43 | ), 44 | [assets] 45 | ) 46 | 47 | return ( 48 | 49 | 50 | 51 | {assets.slice(0, 3).map(({ tokenInfo }) => { 52 | return ( 53 | 59 | ) 60 | })} 61 | 62 | {assets.length} more tokens 63 | 64 | 68 | 50 | 58 | 59 | 60 | 61 | Sorting order 62 | 63 | 64 | 72 | 80 | 81 | 82 | } 83 | iconRight={} 84 | > 85 | {getSortByLabel()} 86 | 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /features/liquidity/components/LiquidityHeader.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowUp, Button, IconWrapper, Inline, styled, Text } from 'junoblocks' 2 | import Link from 'next/link' 3 | import React from 'react' 4 | 5 | export const LiquidityHeader = ({ tokenA, tokenB, size = 'large' }) => { 6 | if (size === 'small') { 7 | return ( 8 | <> 9 | 14 | Pool {tokenA.name} + {tokenB.name} 15 | 16 | 17 | ) 18 | } 19 | 20 | return ( 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 35 | Pool {tokenA.name} + {tokenB.name} 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const StyledNavElement = styled('div', { 43 | display: 'flex', 44 | variants: { 45 | position: { 46 | left: { 47 | flex: 0.1, 48 | justifyContent: 'flex-start', 49 | }, 50 | center: { 51 | flex: 0.8, 52 | justifyContent: 'center', 53 | }, 54 | right: { 55 | flex: 0.1, 56 | justifyContent: 'flex-end', 57 | }, 58 | }, 59 | }, 60 | }) 61 | -------------------------------------------------------------------------------- /features/liquidity/components/ManagePoolDialog/TokenToTokenRates.tsx: -------------------------------------------------------------------------------- 1 | import { usePriceForOneToken, useTxRates } from 'features/swap' 2 | import { 3 | dollarValueFormatterWithDecimals, 4 | formatTokenBalance, 5 | styled, 6 | Text, 7 | } from 'junoblocks' 8 | 9 | export const TokenToTokenRates = ({ 10 | tokenASymbol, 11 | tokenBSymbol, 12 | tokenAAmount, 13 | isLoading, 14 | }) => { 15 | const [oneTokenToTokenPrice] = usePriceForOneToken({ 16 | tokenASymbol, 17 | tokenBSymbol, 18 | }) 19 | 20 | const { isShowing, conversionRate, conversionRateInDollar, dollarValue } = 21 | useTxRates({ 22 | tokenASymbol, 23 | tokenBSymbol, 24 | tokenAAmount, 25 | tokenToTokenPrice: oneTokenToTokenPrice * tokenAAmount, 26 | isLoading, 27 | }) 28 | 29 | return ( 30 | 31 | 32 | 1 {tokenASymbol} ≈ {formatTokenBalance(conversionRate)} {tokenBSymbol} 33 | {' ≈ '}$ 34 | {dollarValueFormatterWithDecimals(conversionRateInDollar, { 35 | includeCommaSeparation: true, 36 | })} 37 | 38 | 39 | $ 40 | {dollarValueFormatterWithDecimals(dollarValue * 2, { 41 | includeCommaSeparation: true, 42 | })} 43 | 44 | 45 | ) 46 | } 47 | 48 | const StyledDivForGrid = styled('div', { 49 | display: 'flex', 50 | justifyContent: 'space-between', 51 | alignItems: 'center', 52 | variants: { 53 | active: { 54 | true: { 55 | opacity: 1, 56 | }, 57 | false: { 58 | opacity: 0, 59 | }, 60 | }, 61 | }, 62 | }) 63 | -------------------------------------------------------------------------------- /features/liquidity/components/ManagePoolDialog/index.ts: -------------------------------------------------------------------------------- 1 | export { ManagePoolDialog } from './ManagePoolDialog' 2 | -------------------------------------------------------------------------------- /features/liquidity/components/PercentageSelection.tsx: -------------------------------------------------------------------------------- 1 | import { Button, styled } from 'junoblocks' 2 | 3 | type PercentageSelectionProps = { 4 | maxLiquidity: number 5 | liquidity: number 6 | onChangeLiquidity: (liquidity: number) => void 7 | } 8 | 9 | export const PercentageSelection = ({ 10 | liquidity, 11 | onChangeLiquidity, 12 | maxLiquidity, 13 | }: PercentageSelectionProps): JSX.Element => { 14 | const valuesForSteps = [0.1, 0.25, 0.5, 0.75, 1] 15 | const percentage = liquidity / maxLiquidity 16 | 17 | return ( 18 | 19 | {valuesForSteps.map((valueForStep) => { 20 | return ( 21 | 32 | ) 33 | })} 34 | 35 | ) 36 | } 37 | 38 | const StyledDivForGrid = styled('div', { 39 | display: 'flex', 40 | alignItems: 'center', 41 | justifyContent: 'space-between', 42 | }) 43 | -------------------------------------------------------------------------------- /features/liquidity/components/SegmentedRewardsSimulator.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | dollarValueFormatterWithDecimals, 3 | protectAgainstNaN, 4 | SegmentedControl, 5 | Text, 6 | } from 'junoblocks' 7 | import { useRef, useState } from 'react' 8 | 9 | type SegmentedRewardsSimulatorProps = { 10 | interestOnStakedBalance: number 11 | stakedLiquidityDollarValue: number 12 | } 13 | 14 | export const SegmentedRewardsSimulator = ({ 15 | interestOnStakedBalance, 16 | stakedLiquidityDollarValue, 17 | }: SegmentedRewardsSimulatorProps) => { 18 | const values = useRef([ 19 | { value: 'year', label: 'Year' }, 20 | { value: 'month', label: 'Month' }, 21 | { value: 'day', label: 'Day' }, 22 | ]).current 23 | 24 | const [activeValue, setActiveValue] = 25 | useState('year') 26 | 27 | const hasStakedLiquidity = stakedLiquidityDollarValue > 0 28 | 29 | const yearRewardOnStakedBalance = 30 | stakedLiquidityDollarValue * interestOnStakedBalance 31 | 32 | let divider = 1 33 | if (activeValue === 'month') { 34 | divider = 12 35 | } else if (activeValue === 'day') { 36 | divider = 365 37 | } 38 | 39 | const rewardsAmountInDollarValue = dollarValueFormatterWithDecimals( 40 | protectAgainstNaN(yearRewardOnStakedBalance / divider), 41 | { includeCommaSeparation: true } 42 | ) 43 | 44 | return ( 45 | <> 46 | { 50 | event.stopPropagation() 51 | setActiveValue(value) 52 | }} 53 | /> 54 | 55 | 60 | {hasStakedLiquidity 61 | ? `+ $${rewardsAmountInDollarValue} /${activeValue}` 62 | : '-- /day'} 63 | 64 | 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /features/liquidity/components/StateSwitchButtons.tsx: -------------------------------------------------------------------------------- 1 | import { Button, styled } from 'junoblocks' 2 | 3 | type StateSwitchButtonsProps = { 4 | activeValue: string 5 | values: Array 6 | onStateChange: (state: string) => void 7 | } 8 | 9 | export const StateSwitchButtons = ({ 10 | activeValue, 11 | values, 12 | onStateChange, 13 | }: StateSwitchButtonsProps) => { 14 | return ( 15 | 16 | {values.map((value) => ( 17 | 27 | ))} 28 | 29 | ) 30 | } 31 | 32 | const StyledDivForGrid = styled('div', { 33 | display: 'flex', 34 | alignItems: 'center', 35 | columnGap: '4px', 36 | '& *': { 37 | textTransform: 'capitalize', 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /features/liquidity/components/StepIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Inline, Text } from 'junoblocks' 2 | 3 | export const StepIcon = ({ step }) => { 4 | return ( 5 | 12 | 13 | {step} 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /features/liquidity/components/UnderlyingAssetRow.tsx: -------------------------------------------------------------------------------- 1 | import { useTokenDollarValue } from 'hooks/useTokenDollarValue' 2 | import { 3 | Button, 4 | dollarValueFormatterWithDecimals, 5 | formatTokenBalance, 6 | ImageForTokenLogo, 7 | InfoIcon, 8 | Inline, 9 | protectAgainstNaN, 10 | Text, 11 | Tooltip, 12 | } from 'junoblocks' 13 | 14 | import { TokenInfo } from '../../../queries/usePoolsListQuery' 15 | 16 | type UnderlyingAssetRowProps = { 17 | tokenInfo?: TokenInfo 18 | tokenAmount?: number 19 | visible?: boolean 20 | symbolVisible?: boolean 21 | } 22 | 23 | export const UnderlyingAssetRow = ({ 24 | tokenInfo, 25 | tokenAmount, 26 | visible = true, 27 | }: // symbolVisible = true, 28 | UnderlyingAssetRowProps) => { 29 | const token = visible ? tokenInfo : undefined 30 | const [tokenDollarValue] = useTokenDollarValue( 31 | visible ? tokenInfo?.symbol : undefined 32 | ) 33 | 34 | const tokenAmountDollarValue = dollarValueFormatterWithDecimals( 35 | protectAgainstNaN(tokenAmount * tokenDollarValue), 36 | { includeCommaSeparation: true } 37 | ) 38 | 39 | const infoTooltipLabel = `≈ $${tokenAmountDollarValue} USD` 40 | 41 | return ( 42 | 47 | 48 | 53 | {token?.symbol} 54 | 55 | 56 | 57 | 58 | {formatTokenBalance(tokenAmount, { includeCommaSeparation: true })} 59 | 60 | {token?.symbol} 61 | 62 | 63 | 37 | 43 | 44 | ) 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /features/swap/components/QueryInput.tsx: -------------------------------------------------------------------------------- 1 | import { Inline, SearchIcon, Text } from 'junoblocks' 2 | import React, { HTMLProps, useEffect, useRef } from 'react' 3 | 4 | type QueryInputProps = { 5 | searchQuery: string 6 | onQueryChange: (query: string) => void 7 | } & HTMLProps 8 | 9 | export const QueryInput = ({ 10 | searchQuery, 11 | onQueryChange, 12 | ...inputProps 13 | }: QueryInputProps) => { 14 | const inputRef = useRef() 15 | 16 | useEffect(() => { 17 | inputRef.current.focus() 18 | 19 | return () => { 20 | onQueryChange('') 21 | } 22 | }, [onQueryChange]) 23 | 24 | return ( 25 | 26 | 27 | 28 | onQueryChange(value)} 35 | autoComplete="off" 36 | style={{ width: '100%' }} 37 | {...inputProps} 38 | /> 39 | 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /features/swap/components/SelectorInput.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | calculateCharactersLength, 3 | styled, 4 | Text, 5 | useAmountChangeController, 6 | } from 'junoblocks' 7 | import React, { HTMLProps, Ref } from 'react' 8 | 9 | type SelectorInputProps = { 10 | amount: number 11 | disabled: boolean 12 | onAmountChange: (amount: number) => void 13 | inputRef?: Ref 14 | } & Omit, 'ref'> 15 | 16 | export const SelectorInput = ({ 17 | amount, 18 | disabled, 19 | onAmountChange, 20 | inputRef, 21 | ...inputProps 22 | }: SelectorInputProps) => { 23 | const { value, setValue } = useAmountChangeController({ 24 | amount, 25 | onAmountChange, 26 | }) 27 | 28 | return ( 29 | 30 | setValue(value) : undefined 39 | } 40 | autoComplete="off" 41 | readOnly={disabled} 42 | style={{ width: `${calculateCharactersLength(value)}ch` }} 43 | {...inputProps} 44 | /> 45 | 46 | ) 47 | } 48 | 49 | const StyledInput = styled('input', { 50 | width: 'auto', 51 | textAlign: 'right', 52 | color: 'inherit', 53 | }) 54 | -------------------------------------------------------------------------------- /features/swap/components/SelectorToggle.tsx: -------------------------------------------------------------------------------- 1 | import { useTokenInfo } from 'hooks/useTokenInfo' 2 | import { 3 | ButtonForWrapper, 4 | Chevron, 5 | formatTokenBalance, 6 | IconWrapper, 7 | ImageForTokenLogo, 8 | styled, 9 | Text, 10 | } from 'junoblocks' 11 | import React from 'react' 12 | import { getPropsForInteractiveElement } from 'util/getPropsForInteractiveElement' 13 | 14 | type SelectorToggleProps = { 15 | isSelecting: boolean 16 | onToggle: () => void 17 | tokenSymbol: string 18 | availableAmount: number 19 | } 20 | 21 | export const SelectorToggle = ({ 22 | isSelecting, 23 | onToggle, 24 | availableAmount, 25 | tokenSymbol, 26 | }: SelectorToggleProps) => { 27 | const { logoURI } = useTokenInfo(tokenSymbol) || {} 28 | 29 | const formattedAvailableAmount = formatTokenBalance(availableAmount, { 30 | includeCommaSeparation: true, 31 | }) 32 | 33 | const hasTokenSelected = Boolean(tokenSymbol) 34 | 35 | return ( 36 | 41 | {(isSelecting || !hasTokenSelected) && ( 42 | <> 43 | Select a token 44 | } 49 | /> 50 | 51 | )} 52 | {!isSelecting && hasTokenSelected && ( 53 | <> 54 | 55 |
56 | {tokenSymbol} 57 | 58 | {formattedAvailableAmount} available 59 | 60 |
61 | } 66 | /> 67 | 68 | )} 69 |
70 | ) 71 | } 72 | 73 | const StyledDivForSelector = styled(ButtonForWrapper, { 74 | cursor: 'pointer', 75 | display: 'grid', 76 | alignItems: 'center', 77 | backgroundColor: '$colors$dark0', 78 | borderRadius: '$1', 79 | transition: 'background-color .1s ease-out', 80 | userSelect: 'none', 81 | whiteSpace: 'pre', 82 | 83 | variants: { 84 | state: { 85 | selected: { 86 | padding: '$4 $6', 87 | columnGap: '$space$6', 88 | gridTemplateColumns: '$space$15 1fr $space$8', 89 | minWidth: 231, 90 | }, 91 | selecting: { 92 | margin: '$space$1 0', 93 | padding: '$space$6 $8', 94 | columnGap: '$space$4', 95 | gridTemplateColumns: '1fr $space$8', 96 | }, 97 | }, 98 | }, 99 | }) 100 | -------------------------------------------------------------------------------- /features/swap/components/TokenOptionsList.tsx: -------------------------------------------------------------------------------- 1 | import { TokenSelectList, TokenSelectListProps } from 'components' 2 | import { useTokenList } from 'hooks/useTokenList' 3 | 4 | export const TokenOptionsList = ({ 5 | activeTokenSymbol, 6 | onSelect, 7 | ...props 8 | }: Omit) => { 9 | const [tokenList] = useTokenList() 10 | return ( 11 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /features/swap/components/index.ts: -------------------------------------------------------------------------------- 1 | export { TokenSwapModule } from './TokenSwapModule' 2 | -------------------------------------------------------------------------------- /features/swap/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { usePriceForOneToken } from './usePriceForOneToken' 2 | export { useTokenSwap } from './useTokenSwap' 3 | export { useTokenToTokenPrice } from './useTokenToTokenPrice' 4 | export { useTxRates } from './useTxRates' 5 | export { useSwapFee } from './useSwapFee' 6 | -------------------------------------------------------------------------------- /features/swap/hooks/usePriceForOneToken.ts: -------------------------------------------------------------------------------- 1 | import { usePersistance } from 'junoblocks' 2 | 3 | import { useTokenToTokenPrice } from './useTokenToTokenPrice' 4 | 5 | export const usePriceForOneToken = ({ tokenASymbol, tokenBSymbol }) => { 6 | const [{ price: currentTokenPrice }, isPriceLoading] = useTokenToTokenPrice({ 7 | tokenASymbol: tokenASymbol, 8 | tokenBSymbol: tokenBSymbol, 9 | tokenAmount: 1, 10 | }) 11 | 12 | const persistPrice = usePersistance( 13 | isPriceLoading ? undefined : currentTokenPrice 14 | ) 15 | 16 | return [persistPrice, isPriceLoading] as const 17 | } 18 | -------------------------------------------------------------------------------- /features/swap/hooks/useSwapFee.ts: -------------------------------------------------------------------------------- 1 | import { NETWORK_FEE } from 'util/constants' 2 | import { useCosmWasmClient } from '../../../hooks/useCosmWasmClient' 3 | import { getSwapFee, FeeResponse } from '../../../services/swap' 4 | import { useTokenToTokenPrice } from './' 5 | import { usePoolsListQuery } from '../../../queries/usePoolsListQuery' 6 | import { useQuery } from 'react-query' 7 | 8 | export const useSwapFee = ({ tokenA, tokenB }) => { 9 | const [tokenToTokenPrice] = useTokenToTokenPrice({ 10 | tokenASymbol: tokenA.tokenSymbol, 11 | tokenBSymbol: tokenB.tokenSymbol, 12 | tokenAmount: 1, 13 | }) 14 | 15 | // pool_id for direct token swap pools OR inputPool for passthrough swap 16 | let poolId1: string = '' 17 | // pool_id for outputPool if passthrough swap 18 | let poolId2: string = '' 19 | if (tokenToTokenPrice.poolForDirectTokenAToTokenBSwap) { 20 | poolId1 = tokenToTokenPrice.poolForDirectTokenAToTokenBSwap.pool_id 21 | } else if (tokenToTokenPrice.poolForDirectTokenBToTokenASwap) { 22 | poolId1 = tokenToTokenPrice.poolForDirectTokenBToTokenASwap.pool_id 23 | } else if (tokenToTokenPrice.passThroughPools) { 24 | poolId1 = tokenToTokenPrice.passThroughPools[0].inputPool.pool_id 25 | poolId2 = tokenToTokenPrice.passThroughPools[0].outputPool.pool_id 26 | } 27 | const { data: fee1 } = useSwapFeeQuery(poolId1) 28 | const { data: fee2 } = useSwapFeeQuery(poolId2) 29 | 30 | // default to NETWORK_FEE constant 31 | let swapFee = NETWORK_FEE * 100 32 | 33 | // use fee for direct token swap or inputPool passthrough if set 34 | if (fee1 && (fee1.lp_fee_percent || fee1.protocol_fee_percent)) { 35 | swapFee = 36 | Number(fee1.lp_fee_percent || 0) + Number(fee1.protocol_fee_percent || 0) 37 | } 38 | // add fee for outputPool passthrough pool if set 39 | if (fee2 && (fee2.lp_fee_percent || fee2.protocol_fee_percent)) { 40 | swapFee += 41 | Number(fee2.lp_fee_percent || 0) + Number(fee2.protocol_fee_percent || 0) 42 | 43 | // add default network fee if not set in outputPool passhthrough 44 | } else if (poolId2 !== '') { 45 | swapFee += NETWORK_FEE * 100 46 | } 47 | 48 | return swapFee 49 | } 50 | 51 | export const useSwapFeeQuery = (poolId: string, options = {}) => { 52 | const { data: poolsListResponse } = usePoolsListQuery() 53 | const client = useCosmWasmClient() 54 | const pool = poolsListResponse?.poolsById[poolId] 55 | 56 | return useQuery( 57 | [`swapFee/${poolId}`], 58 | () => getSwapFee(pool.swap_address, client), 59 | { 60 | enabled: Boolean(client && poolId && pool), 61 | refetchOnMount: false, 62 | ...options, 63 | } 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /features/swap/hooks/useTokenToTokenPrice.tsx: -------------------------------------------------------------------------------- 1 | import { useTokenInfo } from 'hooks/useTokenInfo' 2 | import { usePersistance } from 'junoblocks' 3 | import { useQueryMatchingPoolsForSwap } from 'queries/useQueryMatchingPoolForSwap' 4 | import { useQuery } from 'react-query' 5 | import { DEFAULT_TOKEN_BALANCE_REFETCH_INTERVAL } from 'util/constants' 6 | 7 | import { useCosmWasmClient } from '../../../hooks/useCosmWasmClient' 8 | import { tokenToTokenPriceQueryWithPools } from '../../../queries/tokenToTokenPriceQuery' 9 | import { TokenInfo } from '../../../queries/usePoolsListQuery' 10 | 11 | type UseTokenPairsPricesArgs = { 12 | tokenASymbol: TokenInfo['symbol'] 13 | tokenBSymbol: TokenInfo['symbol'] 14 | tokenAmount: number 15 | enabled?: boolean 16 | refetchInBackground?: boolean 17 | } 18 | 19 | export const useTokenToTokenPriceQuery = ({ 20 | tokenAmount, 21 | tokenASymbol, 22 | tokenBSymbol, 23 | enabled = true, 24 | refetchInBackground, 25 | }: UseTokenPairsPricesArgs) => { 26 | const client = useCosmWasmClient() 27 | 28 | const tokenA = useTokenInfo(tokenASymbol) 29 | const tokenB = useTokenInfo(tokenBSymbol) 30 | 31 | const [matchingPools] = useQueryMatchingPoolsForSwap({ tokenA, tokenB }) 32 | 33 | return useQuery( 34 | `tokenToTokenPrice/${tokenBSymbol}/${tokenASymbol}/${tokenAmount}`, 35 | async () => { 36 | if (tokenA && tokenB && matchingPools) { 37 | return await tokenToTokenPriceQueryWithPools({ 38 | matchingPools, 39 | tokenA, 40 | tokenB, 41 | client, 42 | amount: tokenAmount, 43 | }) 44 | } 45 | }, 46 | { 47 | enabled: Boolean( 48 | enabled && 49 | client && 50 | matchingPools && 51 | tokenA && 52 | tokenB && 53 | tokenAmount > 0 && 54 | tokenBSymbol !== tokenASymbol 55 | ), 56 | refetchOnMount: false, 57 | refetchInterval: refetchInBackground 58 | ? DEFAULT_TOKEN_BALANCE_REFETCH_INTERVAL 59 | : undefined, 60 | refetchIntervalInBackground: Boolean(refetchInBackground), 61 | } 62 | ) 63 | } 64 | 65 | export const useTokenToTokenPrice = (args: UseTokenPairsPricesArgs) => { 66 | const { data: currentTokenPrice, isLoading } = useTokenToTokenPriceQuery(args) 67 | /* persist token price when querying a new one */ 68 | const persistTokenPrice = usePersistance( 69 | isLoading ? undefined : currentTokenPrice 70 | ) 71 | /* select token price */ 72 | const tokenPrice = isLoading ? persistTokenPrice : currentTokenPrice 73 | return [tokenPrice || { price: 0 }, isLoading] as const 74 | } 75 | -------------------------------------------------------------------------------- /features/swap/hooks/useTxRates.ts: -------------------------------------------------------------------------------- 1 | import { useTokenDollarValue } from 'hooks/useTokenDollarValue' 2 | import { usePersistance } from 'junoblocks' 3 | import { protectAgainstNaN } from 'util/conversion' 4 | 5 | import { usePriceForOneToken } from './usePriceForOneToken' 6 | 7 | function calculateTokenToTokenConversionRate({ 8 | tokenAAmount, 9 | tokenToTokenPrice, 10 | oneTokenToTokenPrice, 11 | }) { 12 | if (tokenAAmount === 0) { 13 | return oneTokenToTokenPrice 14 | } 15 | 16 | return tokenToTokenPrice / tokenAAmount 17 | } 18 | 19 | function calculateTokenToTokenConversionDollarRate({ 20 | conversionRate, 21 | tokenADollarPrice, 22 | oneTokenToTokenPrice, 23 | tokenAAmount, 24 | }) { 25 | if (tokenAAmount === 0) { 26 | return tokenADollarPrice 27 | } 28 | 29 | return (tokenADollarPrice * conversionRate) / oneTokenToTokenPrice 30 | } 31 | 32 | export const useTxRates = ({ 33 | tokenASymbol, 34 | tokenBSymbol, 35 | tokenAAmount, 36 | tokenToTokenPrice, 37 | isLoading, 38 | }) => { 39 | const [tokenADollarPrice, fetchingTokenDollarPrice] = 40 | useTokenDollarValue(tokenASymbol) 41 | 42 | const [oneTokenToTokenPrice] = usePriceForOneToken({ 43 | tokenASymbol: tokenASymbol, 44 | tokenBSymbol: tokenBSymbol, 45 | }) 46 | 47 | const dollarValue = (tokenADollarPrice || 0) * (tokenAAmount || 0) 48 | 49 | const shouldShowRates = 50 | (tokenASymbol && 51 | tokenBSymbol && 52 | tokenToTokenPrice > 0 && 53 | typeof tokenAAmount === 'number' && 54 | typeof tokenToTokenPrice === 'number') || 55 | (oneTokenToTokenPrice && tokenAAmount === 0) 56 | 57 | const conversionRate = usePersistance( 58 | isLoading || fetchingTokenDollarPrice || !shouldShowRates 59 | ? undefined 60 | : protectAgainstNaN( 61 | calculateTokenToTokenConversionRate({ 62 | tokenAAmount, 63 | tokenToTokenPrice, 64 | oneTokenToTokenPrice, 65 | }) 66 | ) 67 | ) 68 | 69 | const conversionRateInDollar = usePersistance( 70 | isLoading || fetchingTokenDollarPrice || !shouldShowRates 71 | ? undefined 72 | : protectAgainstNaN( 73 | calculateTokenToTokenConversionDollarRate({ 74 | tokenAAmount, 75 | conversionRate, 76 | tokenADollarPrice, 77 | oneTokenToTokenPrice, 78 | }) 79 | ) 80 | ) 81 | 82 | return { 83 | isShowing: Boolean(shouldShowRates), 84 | conversionRate, 85 | conversionRateInDollar, 86 | dollarValue, 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /features/swap/index.ts: -------------------------------------------------------------------------------- 1 | export { TokenSwapModule } from './components' 2 | export { usePriceForOneToken, useTokenToTokenPrice, useTxRates } from './hooks' 3 | -------------------------------------------------------------------------------- /features/swap/swapAtoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil' 2 | 3 | export type TokenItemState = { 4 | tokenSymbol: string 5 | amount: number 6 | } 7 | 8 | export const tokenSwapAtom = atom<[TokenItemState, TokenItemState]>({ 9 | key: 'tokenSwap', 10 | default: [ 11 | { 12 | tokenSymbol: null, 13 | amount: 0, 14 | }, 15 | { 16 | tokenSymbol: null, 17 | amount: 0, 18 | }, 19 | ], 20 | effects_UNSTABLE: [ 21 | function validateIfTokensAreSame({ onSet, setSelf }) { 22 | onSet((newValue, oldValue) => { 23 | const [tokenA, tokenB] = newValue 24 | if (tokenA.tokenSymbol === tokenB.tokenSymbol) { 25 | requestAnimationFrame(() => { 26 | setSelf([oldValue[1], oldValue[0]]) 27 | }) 28 | } 29 | }) 30 | }, 31 | ], 32 | }) 33 | 34 | export const slippageAtom = atom({ 35 | key: 'slippageForSwap', 36 | default: 0.01, 37 | }) 38 | -------------------------------------------------------------------------------- /hooks/useBondTokens.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from 'react-query' 2 | import { useRecoilValue } from 'recoil' 3 | 4 | import { usePoolFromListQueryById } from '../queries/usePoolsListQuery' 5 | import { stakeTokens, unstakeTokens } from '../services/staking' 6 | import { walletState } from '../state/atoms/walletAtoms' 7 | import { useSwapInfo } from './useSwapInfo' 8 | 9 | type UseBondTokensArgs = { 10 | poolId: string 11 | } & Parameters[2] 12 | 13 | export const useBondTokens = ({ poolId, ...options }: UseBondTokensArgs) => { 14 | const [pool] = usePoolFromListQueryById({ poolId }) 15 | const { address, client } = useRecoilValue(walletState) 16 | const [swap] = useSwapInfo({ poolId }) 17 | 18 | return useMutation(async (amount: number) => { 19 | return stakeTokens( 20 | address, 21 | pool.staking_address, 22 | swap.lp_token_address, 23 | amount, 24 | client 25 | ) 26 | }, options) 27 | } 28 | 29 | type UseUnbondTokensArgs = { 30 | poolId: string 31 | } & Parameters[2] 32 | 33 | export const useUnbondTokens = ({ 34 | poolId, 35 | ...options 36 | }: UseUnbondTokensArgs) => { 37 | const [pool] = usePoolFromListQueryById({ poolId }) 38 | const { address, client } = useRecoilValue(walletState) 39 | 40 | return useMutation(async (amount: number) => { 41 | return unstakeTokens(address, pool.staking_address, amount, client) 42 | }, options) 43 | } 44 | -------------------------------------------------------------------------------- /hooks/useChainInfo.ts: -------------------------------------------------------------------------------- 1 | import { ChainInfo } from '@keplr-wallet/types' 2 | import { useQuery } from 'react-query' 3 | 4 | import { queryClient } from '../services/queryClient' 5 | 6 | const chainInfoQueryKey = '@chain-info' 7 | 8 | export const unsafelyReadChainInfoCache = () => 9 | queryClient.getQueryCache().find(chainInfoQueryKey)?.state?.data as 10 | | ChainInfo 11 | | undefined 12 | 13 | export const useChainInfo = () => { 14 | const { data, isLoading } = useQuery( 15 | chainInfoQueryKey, 16 | async () => { 17 | const response = await fetch(process.env.NEXT_PUBLIC_CHAIN_INFO_URL) 18 | return await response.json() 19 | }, 20 | { 21 | onError(e) { 22 | console.error('Error loading chain info:', e) 23 | }, 24 | } 25 | ) 26 | 27 | return [data, isLoading] as const 28 | } 29 | -------------------------------------------------------------------------------- /hooks/useConnectIBCWallet.ts: -------------------------------------------------------------------------------- 1 | import { createWasmAminoConverters } from '@cosmjs/cosmwasm-stargate' 2 | import { 3 | AminoTypes, 4 | createIbcAminoConverters, 5 | GasPrice, 6 | SigningStargateClient, 7 | } from '@cosmjs/stargate' 8 | import { useEffect } from 'react' 9 | import { useMutation } from 'react-query' 10 | import { useRecoilState } from 'recoil' 11 | 12 | import { ibcWalletState, WalletStatusType } from '../state/atoms/walletAtoms' 13 | import { GAS_PRICE } from '../util/constants' 14 | import { useIBCAssetInfo } from './useIBCAssetInfo' 15 | 16 | /* shares very similar logic with `useConnectWallet` and is a subject to refactor */ 17 | export const useConnectIBCWallet = ( 18 | tokenSymbol: string, 19 | mutationOptions?: Parameters[2] 20 | ) => { 21 | const [{ status, tokenSymbol: storedTokenSymbol }, setWalletState] = 22 | useRecoilState(ibcWalletState) 23 | 24 | const assetInfo = useIBCAssetInfo(tokenSymbol || storedTokenSymbol) 25 | 26 | const mutation = useMutation(async () => { 27 | if (window && !window?.keplr) { 28 | alert('Please install Keplr extension and refresh the page.') 29 | return 30 | } 31 | 32 | if (!tokenSymbol && !storedTokenSymbol) { 33 | throw new Error( 34 | 'You must provide `tokenSymbol` before connecting to the wallet.' 35 | ) 36 | } 37 | 38 | if (!assetInfo) { 39 | throw new Error( 40 | 'Asset info for the provided `tokenSymbol` was not found. Check your internet connection.' 41 | ) 42 | } 43 | 44 | /* set the fetching state */ 45 | setWalletState((value) => ({ 46 | ...value, 47 | tokenSymbol, 48 | client: null, 49 | state: WalletStatusType.connecting, 50 | })) 51 | 52 | try { 53 | const { chain_id, rpc } = assetInfo 54 | 55 | await window.keplr.enable(chain_id) 56 | const offlineSigner = await window.getOfflineSignerAuto(chain_id) 57 | 58 | const wasmChainClient = await SigningStargateClient.connectWithSigner( 59 | rpc, 60 | offlineSigner, 61 | { 62 | gasPrice: GasPrice.fromString(GAS_PRICE), 63 | /* 64 | * passing ibc amino types for all the amino signers (eg ledger, wallet connect) 65 | * to enable ibc & wasm transactions 66 | * */ 67 | aminoTypes: new AminoTypes( 68 | Object.assign( 69 | createIbcAminoConverters(), 70 | createWasmAminoConverters() 71 | ) 72 | ), 73 | } 74 | ) 75 | 76 | const [{ address }] = await offlineSigner.getAccounts() 77 | 78 | /* successfully update the wallet state */ 79 | setWalletState({ 80 | tokenSymbol, 81 | address, 82 | client: wasmChainClient, 83 | status: WalletStatusType.connected, 84 | }) 85 | } catch (e) { 86 | /* set the error state */ 87 | setWalletState({ 88 | tokenSymbol: null, 89 | address: '', 90 | client: null, 91 | status: WalletStatusType.error, 92 | }) 93 | 94 | throw e 95 | } 96 | }, mutationOptions) 97 | 98 | const connectWallet = mutation.mutate 99 | 100 | useEffect(() => { 101 | /* restore wallet connection */ 102 | if (status === WalletStatusType.restored && assetInfo) { 103 | connectWallet(null) 104 | } 105 | }, [status, connectWallet, assetInfo]) 106 | 107 | useEffect(() => { 108 | function reconnectWallet() { 109 | if (assetInfo && status === WalletStatusType.connected) { 110 | connectWallet(null) 111 | } 112 | } 113 | 114 | window.addEventListener('keplr_keystorechange', reconnectWallet) 115 | return () => { 116 | window.removeEventListener('keplr_keystorechange', reconnectWallet) 117 | } 118 | }, [connectWallet, status, assetInfo]) 119 | 120 | return mutation 121 | } 122 | -------------------------------------------------------------------------------- /hooks/useConnectWallet.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createWasmAminoConverters, 3 | SigningCosmWasmClient, 4 | } from '@cosmjs/cosmwasm-stargate' 5 | import { 6 | AminoTypes, 7 | createIbcAminoConverters, 8 | GasPrice, 9 | } from '@cosmjs/stargate' 10 | import { useEffect } from 'react' 11 | import { useMutation } from 'react-query' 12 | import { useRecoilState } from 'recoil' 13 | 14 | import { walletState, WalletStatusType } from '../state/atoms/walletAtoms' 15 | import { GAS_PRICE } from '../util/constants' 16 | import { useChainInfo } from './useChainInfo' 17 | 18 | export const useConnectWallet = ( 19 | mutationOptions?: Parameters[2] 20 | ) => { 21 | const [{ status }, setWalletState] = useRecoilState(walletState) 22 | const [chainInfo] = useChainInfo() 23 | 24 | const mutation = useMutation(async () => { 25 | if (window && !window?.keplr) { 26 | alert('Please install Keplr extension and refresh the page.') 27 | return 28 | } 29 | 30 | /* set the fetching state */ 31 | setWalletState((value) => ({ 32 | ...value, 33 | client: null, 34 | state: WalletStatusType.connecting, 35 | })) 36 | 37 | try { 38 | await window.keplr.experimentalSuggestChain(chainInfo) 39 | await window.keplr.enable(chainInfo.chainId) 40 | 41 | const offlineSigner = await window.getOfflineSignerAuto(chainInfo.chainId) 42 | const wasmChainClient = await SigningCosmWasmClient.connectWithSigner( 43 | chainInfo.rpc, 44 | offlineSigner, 45 | { 46 | gasPrice: GasPrice.fromString(GAS_PRICE), 47 | /* 48 | * passing ibc amino types for all the amino signers (eg ledger, wallet connect) 49 | * to enable ibc & wasm transactions 50 | * */ 51 | aminoTypes: new AminoTypes( 52 | Object.assign( 53 | createIbcAminoConverters(), 54 | createWasmAminoConverters() 55 | ) 56 | ), 57 | } 58 | ) 59 | 60 | const [{ address }] = await offlineSigner.getAccounts() 61 | const key = await window.keplr.getKey(chainInfo.chainId) 62 | 63 | /* successfully update the wallet state */ 64 | setWalletState({ 65 | key, 66 | address, 67 | client: wasmChainClient, 68 | status: WalletStatusType.connected, 69 | }) 70 | } catch (e) { 71 | /* set the error state */ 72 | setWalletState({ 73 | key: null, 74 | address: '', 75 | client: null, 76 | status: WalletStatusType.error, 77 | }) 78 | 79 | /* throw the error for the UI */ 80 | throw e 81 | } 82 | }, mutationOptions) 83 | 84 | useEffect( 85 | function restoreWalletConnectionIfHadBeenConnectedBefore() { 86 | /* restore wallet connection if the state has been set with the */ 87 | if (chainInfo?.rpc && status === WalletStatusType.restored) { 88 | mutation.mutate(null) 89 | } 90 | }, // eslint-disable-next-line 91 | [status, chainInfo?.rpc] 92 | ) 93 | 94 | useEffect( 95 | function listenToWalletAddressChangeInKeplr() { 96 | function reconnectWallet() { 97 | if (status === WalletStatusType.connected) { 98 | mutation.mutate(null) 99 | } 100 | } 101 | 102 | window.addEventListener('keplr_keystorechange', reconnectWallet) 103 | return () => { 104 | window.removeEventListener('keplr_keystorechange', reconnectWallet) 105 | } 106 | }, 107 | // eslint-disable-next-line 108 | [status] 109 | ) 110 | 111 | return mutation 112 | } 113 | -------------------------------------------------------------------------------- /hooks/useCosmWasmClient.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query' 2 | 3 | import { cosmWasmClientRouter } from '../util/cosmWasmClientRouter' 4 | import { useChainInfo } from './useChainInfo' 5 | 6 | export const useCosmWasmClient = () => { 7 | const [chainInfo] = useChainInfo() 8 | 9 | const { data } = useQuery( 10 | '@cosmwasm-client', 11 | () => cosmWasmClientRouter.connect(chainInfo.rpc), 12 | { enabled: Boolean(chainInfo?.rpc) } 13 | ) 14 | 15 | return data 16 | } 17 | -------------------------------------------------------------------------------- /hooks/useIBCAssetInfo.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react' 2 | 3 | import { IBCAssetInfo, useIBCAssetList } from './useIbcAssetList' 4 | 5 | export const getIBCAssetInfoFromList = ( 6 | assetSymbol: string, 7 | assetList: Array 8 | ): IBCAssetInfo | undefined => assetList?.find((x) => x.symbol === assetSymbol) 9 | 10 | export const useGetMultipleIBCAssetInfo = () => { 11 | const [assetList] = useIBCAssetList() 12 | return useCallback( 13 | function getMultipleIBCAssetInfo(assetSymbols: Array) { 14 | return assetSymbols?.map((assetSymbol) => 15 | getIBCAssetInfoFromList(assetSymbol, assetList?.tokens) 16 | ) 17 | }, 18 | [assetList] 19 | ) 20 | } 21 | 22 | export const useIBCAssetInfo = (assetSymbol: string) => { 23 | const getMultipleIBCAssetInfo = useGetMultipleIBCAssetInfo() 24 | return useMemo( 25 | () => getMultipleIBCAssetInfo([assetSymbol])?.[0], 26 | [assetSymbol, getMultipleIBCAssetInfo] 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /hooks/useIBCTokenBalance.tsx: -------------------------------------------------------------------------------- 1 | import { SigningStargateClient } from '@cosmjs/stargate' 2 | import { useQuery } from 'react-query' 3 | import { useRecoilValue } from 'recoil' 4 | import { convertMicroDenomToDenom } from 'util/conversion' 5 | 6 | import { walletState } from '../state/atoms/walletAtoms' 7 | import { DEFAULT_TOKEN_BALANCE_REFETCH_INTERVAL } from '../util/constants' 8 | import { useIBCAssetInfo } from './useIBCAssetInfo' 9 | 10 | export const useIBCTokenBalance = (tokenSymbol) => { 11 | const { address: nativeWalletAddress } = useRecoilValue(walletState) 12 | const ibcAsset = useIBCAssetInfo(tokenSymbol) 13 | const { data: balance = 0, isLoading } = useQuery( 14 | [`ibcTokenBalance/${tokenSymbol}`, nativeWalletAddress], 15 | async () => { 16 | const { denom, decimals, chain_id, rpc } = ibcAsset 17 | 18 | await window.keplr.enable(chain_id) 19 | const offlineSigner = await window.getOfflineSigner(chain_id) 20 | 21 | const wasmChainClient = await SigningStargateClient.connectWithSigner( 22 | rpc, 23 | offlineSigner 24 | ) 25 | 26 | const [{ address }] = await offlineSigner.getAccounts() 27 | const coin = await wasmChainClient.getBalance(address, denom) 28 | 29 | const amount = coin ? Number(coin.amount) : 0 30 | return convertMicroDenomToDenom(amount, decimals) 31 | }, 32 | { 33 | enabled: Boolean(nativeWalletAddress && ibcAsset), 34 | refetchOnMount: 'always', 35 | refetchInterval: DEFAULT_TOKEN_BALANCE_REFETCH_INTERVAL, 36 | refetchIntervalInBackground: true, 37 | } 38 | ) 39 | 40 | return { balance, isLoading: isLoading } 41 | } 42 | -------------------------------------------------------------------------------- /hooks/useIbcAssetList.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query' 2 | 3 | export type IBCAssetInfo = { 4 | id: string 5 | name: string 6 | symbol: string 7 | chain_id: string 8 | rpc: string 9 | denom: string 10 | decimals: number 11 | juno_denom: string 12 | juno_channel: string 13 | channel: string 14 | logoURI: string 15 | deposit_gas_fee?: number 16 | external_deposit_uri?: string 17 | } 18 | 19 | export type IBCAssetList = { 20 | tokens: Array 21 | } 22 | 23 | export const useIBCAssetList = () => { 24 | const { data, isLoading } = useQuery( 25 | '@ibc-asset-list', 26 | async () => { 27 | const response = await fetch(process.env.NEXT_PUBLIC_IBC_ASSETS_URL) 28 | return await response.json() 29 | }, 30 | { 31 | onError(e) { 32 | console.error('Error loading ibc asset list:', e) 33 | }, 34 | refetchOnMount: false, 35 | refetchIntervalInBackground: true, 36 | refetchInterval: 1000 * 60, 37 | } 38 | ) 39 | 40 | return [data, isLoading] as const 41 | } 42 | -------------------------------------------------------------------------------- /hooks/useQueriesDataSelector.ts: -------------------------------------------------------------------------------- 1 | import { usePersistance } from 'junoblocks' 2 | import { useMemo } from 'react' 3 | import { useQueries } from 'react-query' 4 | 5 | export function useQueriesDataSelector< 6 | TQueries extends ReturnType 7 | >(queriesResult: TQueries) { 8 | const [data, isLoading, isError] = useMemo(() => { 9 | const loading = queriesResult.some( 10 | ({ isLoading, data }) => isLoading && !data 11 | ) 12 | const error = queriesResult.some(({ isError }) => isError) 13 | 14 | const queriesData: Array = queriesResult.map( 15 | ({ data }) => data 16 | ) 17 | 18 | const didFetchEveryQuery = !queriesData.includes(undefined) 19 | 20 | return [ 21 | didFetchEveryQuery ? queriesData : undefined, 22 | loading, 23 | error, 24 | ] as const 25 | }, [queriesResult]) 26 | 27 | const persistData = usePersistance(data?.[0] ? data : undefined) 28 | 29 | return [persistData, isLoading, isError] as const 30 | } 31 | -------------------------------------------------------------------------------- /hooks/useRefetchQueries.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react' 2 | import { useQueryClient } from 'react-query' 3 | 4 | const sleep = (delayMs: number) => 5 | new Promise((resolve) => setTimeout(resolve, delayMs)) 6 | 7 | export const useRefetchQueries = ( 8 | queryKey?: string | Array, 9 | delayMs?: number 10 | ) => { 11 | const queryClient = useQueryClient() 12 | 13 | const queriesToRefetchRef = useRef(queryKey) 14 | queriesToRefetchRef.current = queryKey 15 | 16 | return useCallback( 17 | async function refetchQueries(queryKeyArg?: string | Array) { 18 | const queriesToRefetch = queryKeyArg || queriesToRefetchRef.current 19 | 20 | if (delayMs) { 21 | await sleep(delayMs) 22 | } 23 | 24 | if (Array.isArray(queriesToRefetch)) { 25 | return Promise.all( 26 | queriesToRefetch.map((query) => 27 | queryClient.refetchQueries(new RegExp(query) as unknown as string) 28 | ) 29 | ) 30 | } 31 | 32 | return queryClient.refetchQueries( 33 | typeof queriesToRefetch === 'string' && 34 | (new RegExp(queriesToRefetch) as unknown as string) 35 | ) 36 | }, 37 | [queryClient, delayMs] 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /hooks/useSwapInfo.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query' 2 | 3 | import { usePoolsListQuery } from '../queries/usePoolsListQuery' 4 | import { getSwapInfo, InfoResponse } from '../services/swap' 5 | import { DEFAULT_TOKEN_BALANCE_REFETCH_INTERVAL } from '../util/constants' 6 | import { useCosmWasmClient } from './useCosmWasmClient' 7 | 8 | export type SwapInfo = Pick< 9 | InfoResponse, 10 | 'token1_denom' | 'token2_denom' | 'lp_token_address' 11 | > & { 12 | swap_address: string 13 | lp_token_supply: number 14 | token1_reserve: number 15 | token2_reserve: number 16 | lp_fee_percent?: number 17 | protocol_fee_percent?: number 18 | } 19 | 20 | type UseMultipleSwapInfoArgs = { 21 | poolId?: string 22 | refetchInBackground?: boolean 23 | } 24 | 25 | export const useSwapInfo = ({ 26 | poolId, 27 | refetchInBackground, 28 | }: UseMultipleSwapInfoArgs) => { 29 | const { data: poolsListResponse } = usePoolsListQuery() 30 | const client = useCosmWasmClient() 31 | 32 | const { data, isLoading } = useQuery( 33 | `swapInfo/${poolId}`, 34 | async () => { 35 | const pool = poolsListResponse.poolsById[poolId] 36 | const swap = await getSwapInfo(pool.swap_address, client) 37 | 38 | return { 39 | ...swap, 40 | swap_address: pool.swap_address, 41 | token1_reserve: Number(swap.token1_reserve), 42 | token2_reserve: Number(swap.token2_reserve), 43 | lp_token_supply: Number(swap.lp_token_supply), 44 | lp_fee_percent: swap.lp_fee_percent 45 | ? Number(swap.lp_fee_percent) 46 | : undefined, 47 | protocol_fee_percent: swap.protocol_fee_percent 48 | ? Number(swap.protocol_fee_percent) 49 | : undefined, 50 | } 51 | }, 52 | { 53 | enabled: Boolean(poolsListResponse?.pools.length && client && poolId), 54 | refetchOnMount: false, 55 | refetchInterval: refetchInBackground 56 | ? DEFAULT_TOKEN_BALANCE_REFETCH_INTERVAL 57 | : undefined, 58 | refetchIntervalInBackground: refetchInBackground, 59 | } 60 | ) 61 | 62 | return [data, isLoading] as const 63 | } 64 | -------------------------------------------------------------------------------- /hooks/useTokenDollarValue.tsx: -------------------------------------------------------------------------------- 1 | import { usePriceForOneToken } from 'features/swap' 2 | import { useQuery } from 'react-query' 3 | 4 | import { tokenDollarValueQuery } from '../queries/tokenDollarValueQuery' 5 | import { DEFAULT_TOKEN_BALANCE_REFETCH_INTERVAL } from '../util/constants' 6 | import { useGetMultipleIBCAssetInfo } from './useIBCAssetInfo' 7 | import { 8 | useBaseTokenInfo, 9 | useGetMultipleTokenInfo, 10 | useTokenInfo, 11 | } from './useTokenInfo' 12 | 13 | export const useTokenDollarValue = (tokenSymbol?: string) => { 14 | const { symbol: baseTokenSymbol } = useBaseTokenInfo() || {} 15 | const tokenInfo = useTokenInfo(tokenSymbol) 16 | 17 | const tokenSymbolToLookupDollarValueFor = tokenInfo?.id 18 | ? tokenSymbol 19 | : baseTokenSymbol 20 | 21 | const [[tokenDollarPrice], fetchingTokenDollarPrice] = 22 | useTokenDollarValueQuery( 23 | tokenSymbolToLookupDollarValueFor 24 | ? [tokenSymbolToLookupDollarValueFor] 25 | : null 26 | ) 27 | 28 | const [oneTokenToTokenPrice, fetchingTokenToTokenPrice] = usePriceForOneToken( 29 | { 30 | tokenASymbol: tokenSymbol, 31 | tokenBSymbol: baseTokenSymbol, 32 | } 33 | ) 34 | 35 | /* if the token has an id or it's the baseToken then let's return pure price from the api */ 36 | const shouldRenderPureDollarPrice = 37 | tokenSymbol === baseTokenSymbol || Boolean(tokenInfo?.id) 38 | if (shouldRenderPureDollarPrice) { 39 | return [tokenDollarPrice, fetchingTokenDollarPrice] as const 40 | } 41 | 42 | /* otherwise, let's query the chain and calculate the dollar price based on ratio to base token */ 43 | return [ 44 | tokenDollarPrice * oneTokenToTokenPrice, 45 | fetchingTokenDollarPrice || fetchingTokenToTokenPrice, 46 | ] as const 47 | } 48 | 49 | export const useTokenDollarValueQuery = (tokenSymbols?: Array) => { 50 | const getMultipleTokenInfo = useGetMultipleTokenInfo() 51 | const getMultipleIBCAssetInfo = useGetMultipleIBCAssetInfo() 52 | 53 | const { data, isLoading } = useQuery( 54 | `tokenDollarValue/${tokenSymbols?.join('/')}`, 55 | async (): Promise> => { 56 | const tokenIds = tokenSymbols?.map( 57 | (tokenSymbol) => 58 | ( 59 | getMultipleTokenInfo([tokenSymbol])?.[0] || 60 | getMultipleIBCAssetInfo([tokenSymbol])?.[0] 61 | )?.id 62 | ) 63 | 64 | if (tokenIds) { 65 | return tokenDollarValueQuery(tokenIds) 66 | } 67 | }, 68 | { 69 | enabled: Boolean(tokenSymbols?.length), 70 | refetchOnMount: 'always', 71 | refetchInterval: DEFAULT_TOKEN_BALANCE_REFETCH_INTERVAL, 72 | refetchIntervalInBackground: true, 73 | } 74 | ) 75 | 76 | return [data || [], isLoading] as const 77 | } 78 | -------------------------------------------------------------------------------- /hooks/useTokenInfo.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react' 2 | 3 | import { TokenInfo } from '../queries/usePoolsListQuery' 4 | import { useTokenList } from './useTokenList' 5 | 6 | /* token selector functions */ 7 | export const getBaseTokenFromTokenList = (tokenList): TokenInfo | undefined => 8 | tokenList?.base_token 9 | 10 | export const getTokenInfoFromTokenList = ( 11 | tokenSymbol: string, 12 | tokensList: Array 13 | ): TokenInfo | undefined => tokensList?.find((x) => x.symbol === tokenSymbol) 14 | /* /token selector functions */ 15 | 16 | /* returns a selector for getting multiple tokens info at once */ 17 | export const useGetMultipleTokenInfo = () => { 18 | const [tokenList] = useTokenList() 19 | return useCallback( 20 | (tokenSymbols: Array) => 21 | tokenSymbols?.map((tokenSymbol) => 22 | getTokenInfoFromTokenList(tokenSymbol, tokenList?.tokens) 23 | ), 24 | [tokenList] 25 | ) 26 | } 27 | 28 | /* hook for token info retrieval based on multiple `tokenSymbol` */ 29 | export const useMultipleTokenInfo = (tokenSymbols: Array) => { 30 | const getMultipleTokenInfo = useGetMultipleTokenInfo() 31 | return useMemo( 32 | () => getMultipleTokenInfo(tokenSymbols), 33 | [tokenSymbols, getMultipleTokenInfo] 34 | ) 35 | } 36 | 37 | /* hook for token info retrieval based on `tokenSymbol` */ 38 | export const useTokenInfo = (tokenSymbol: string) => { 39 | return useMultipleTokenInfo(useMemo(() => [tokenSymbol], [tokenSymbol]))?.[0] 40 | } 41 | 42 | /* hook for base token info retrieval */ 43 | export const useBaseTokenInfo = () => { 44 | const [tokenList] = useTokenList() 45 | return useMemo(() => getBaseTokenFromTokenList(tokenList), [tokenList]) 46 | } 47 | -------------------------------------------------------------------------------- /hooks/useTokenList.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query' 2 | 3 | import { TokenInfo, usePoolsListQuery } from '../queries/usePoolsListQuery' 4 | 5 | export type TokenList = { 6 | base_token: TokenInfo 7 | tokens: Array 8 | tokensBySymbol: Map 9 | } 10 | 11 | export const useTokenList = () => { 12 | const { data: poolsListResponse } = usePoolsListQuery() 13 | 14 | /* generate token list off pool list and store it in cache */ 15 | const { data } = useQuery( 16 | '@token-list', 17 | () => { 18 | const tokenMapBySymbol = new Map() 19 | /* serialize pool assets */ 20 | poolsListResponse.pools.forEach(({ pool_assets }) => { 21 | pool_assets?.forEach((token) => { 22 | if (!tokenMapBySymbol.has(token.symbol)) { 23 | tokenMapBySymbol.set(token.symbol, token) 24 | } 25 | }) 26 | }) 27 | 28 | return { 29 | base_token: poolsListResponse.base_token, 30 | tokens: Array.from(tokenMapBySymbol.values()), 31 | tokensBySymbol: tokenMapBySymbol, 32 | } 33 | }, 34 | { 35 | enabled: Boolean(poolsListResponse?.pools), 36 | refetchOnMount: false, 37 | onError(e) { 38 | console.error('Error generating token list:', e) 39 | }, 40 | } 41 | ) 42 | 43 | const isLoading = !poolsListResponse?.pools 44 | 45 | return [data, isLoading] as const 46 | } 47 | -------------------------------------------------------------------------------- /hooks/useWalletConnectionStatus.ts: -------------------------------------------------------------------------------- 1 | import { useRecoilValue } from 'recoil' 2 | 3 | import { 4 | ibcWalletState, 5 | walletState, 6 | WalletStatusType, 7 | } from '../state/atoms/walletAtoms' 8 | 9 | export const useWalletConnectionStatus = ( 10 | wallet: typeof walletState | typeof ibcWalletState 11 | ) => { 12 | const { status: walletStatus } = useRecoilValue(wallet as typeof walletState) 13 | const isConnected = walletStatus === WalletStatusType.connected 14 | const isConnecting = 15 | walletStatus === WalletStatusType.connecting || 16 | walletStatus === WalletStatusType.restored 17 | 18 | return { 19 | isConnecting, 20 | isConnected, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /icons/Analytics.tsx: -------------------------------------------------------------------------------- 1 | export const Analytics = () => ( 2 | 9 | 10 | 15 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /icons/Dollar.tsx: -------------------------------------------------------------------------------- 1 | export const Dollar = () => ( 2 | 9 | 10 | 15 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /icons/Pools.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react' 2 | 3 | export const PoolsIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /icons/PriceData.tsx: -------------------------------------------------------------------------------- 1 | export const PriceData = () => ( 2 | 9 | 10 | 15 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /icons/Swap.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react' 2 | 3 | export const SwapIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /icons/Transfer.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react' 2 | 3 | export const TransferIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Logo' 2 | export * from './LogoBrighter' 3 | export * from './LogoText' 4 | export * from './Transfer' 5 | export * from './Swap' 6 | export * from './Pools' 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest') 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: './', 6 | }) 7 | 8 | // Add any custom config to be passed to Jest 9 | const customJestConfig = { 10 | setupFilesAfterEnv: ['/jest.setup.js'], 11 | moduleNameMapper: { 12 | // Handle module aliases (this will be automatically configured for you soon) 13 | '^@/components/(.*)$': '/components/$1', 14 | 15 | '^@/pages/(.*)$': '/pages/$1', 16 | }, 17 | testEnvironment: 'jest-environment-jsdom', 18 | } 19 | 20 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 21 | module.exports = createJestConfig(customJestConfig) 22 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // Optional: configure or set up a testing framework before each test. 2 | // If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` 3 | 4 | // Used for __tests__/testing-library.js 5 | // Learn more: https://github.com/testing-library/jest-dom 6 | import '@testing-library/jest-dom/extend-expect' 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true', 3 | }) 4 | 5 | const withBundleJunoblocks = require('next-bundle-junoblocks') 6 | 7 | const config = { 8 | reactStrictMode: true, 9 | target: 'serverless', 10 | images: { 11 | loader: 'cloudinary', 12 | path: 'https://res.cloudinary.com/dk8s7xjsl/image/upload/', 13 | }, 14 | webpack(config, { webpack }) { 15 | config.plugins.push( 16 | new webpack.ProvidePlugin({ 17 | Buffer: ['buffer', 'Buffer'], 18 | }) 19 | ) 20 | 21 | if (!config.resolve.fallback) { 22 | config.resolve.fallback = {} 23 | } 24 | 25 | Object.assign(config.resolve.fallback, { 26 | buffer: false, 27 | crypto: false, 28 | events: false, 29 | path: false, 30 | stream: false, 31 | string_decoder: false, 32 | }) 33 | 34 | return config 35 | }, 36 | } 37 | 38 | module.exports = withBundleAnalyzer( 39 | process.env.BUNDLE_JUNOBLOCKS === 'true' 40 | ? withBundleJunoblocks(config) 41 | : config 42 | ) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasmswap-interface", 3 | "version": "1.7.0", 4 | "scripts": { 5 | "dev": "next dev", 6 | "dev:w-blocks": "BUNDLE_JUNOBLOCKS=true next dev", 7 | "build": "next build && next export", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "jest --watch", 11 | "prettier:write": "prettier --write .", 12 | "typecheck": "tsc" 13 | }, 14 | "dependencies": { 15 | "@cosmjs/amino": "0.28.10", 16 | "@cosmjs/cosmwasm": "0.25.6", 17 | "@cosmjs/cosmwasm-stargate": "0.28.10", 18 | "@cosmjs/crypto": "0.28.10", 19 | "@cosmjs/encoding": "0.28.10", 20 | "@cosmjs/launchpad": "0.27.1", 21 | "@cosmjs/math": "0.28.10", 22 | "@cosmjs/proto-signing": "0.28.10", 23 | "@cosmjs/stargate": "0.28.10", 24 | "@next/bundle-analyzer": "^12.1.0", 25 | "@react-spring/web": "^9.4.2", 26 | "@stitches/react": "^1.2.6", 27 | "body-scroll-lock": "^4.0.0-beta.0", 28 | "color": "^4.1.0", 29 | "dayjs": "^1.10.7", 30 | "eslint-plugin-simple-import-sort": "^7.0.0", 31 | "eslint-plugin-testing-library": "^5.5.1", 32 | "focus-visible": "^5.2.0", 33 | "gsap": "^3.7.1", 34 | "junoblocks": "^0.2.7", 35 | "next": "^12.2.2", 36 | "normalize.css": "^8.0.1", 37 | "react": "^18.2.0", 38 | "react-dom": "^18.2.0", 39 | "react-hot-toast": "^2.2.0", 40 | "react-query": "^3.39.1", 41 | "react-use": "^17.4.0", 42 | "recoil": "^0.7.4", 43 | "sass": "^1.45.0" 44 | }, 45 | "devDependencies": { 46 | "@keplr-wallet/types": "^0.9.9", 47 | "@testing-library/jest-dom": "^5.16.4", 48 | "@testing-library/react": "^13.3.0", 49 | "@testing-library/user-event": "^14.2.1", 50 | "@types/react": "^17.0.0", 51 | "@types/react-dom": "^17.0.0", 52 | "autoprefixer": "^10.3.1", 53 | "eslint": "7.30.0", 54 | "eslint-config-next": "11.0.1", 55 | "jest": "^28.1.2", 56 | "jest-environment-jsdom": "^28.1.2", 57 | "next-bundle-junoblocks": "^1.0.2", 58 | "postcss": "^8.3.5", 59 | "prettier": "^2.3.2", 60 | "typescript": "4.5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import 'normalize.css' 2 | import 'styles/globals.scss' 3 | import 'focus-visible' 4 | 5 | import { ErrorBoundary } from 'components/ErrorBoundary' 6 | import { TestnetDialog } from 'components/TestnetDialog' 7 | import { css, media, useMedia } from 'junoblocks' 8 | import type { AppProps } from 'next/app' 9 | import { Toaster } from 'react-hot-toast' 10 | import { QueryClientProvider } from 'react-query' 11 | import { RecoilRoot } from 'recoil' 12 | import { queryClient } from 'services/queryClient' 13 | 14 | import { NextJsAppRoot } from '../components/NextJsAppRoot' 15 | import { __TEST_MODE__ } from '../util/constants' 16 | 17 | const toasterClassName = css({ 18 | [media.sm]: { 19 | width: '100%', 20 | padding: 0, 21 | bottom: '$6 !important', 22 | }, 23 | }).toString() 24 | 25 | function MyApp({ Component, pageProps }: AppProps) { 26 | const isSmallScreen = useMedia('sm') 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | {__TEST_MODE__ && } 34 | 40 | 41 | 42 | 43 | 44 | ) 45 | } 46 | 47 | export default MyApp 48 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 13 | 19 | 25 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 | ) 48 | } 49 | } 50 | 51 | export default MyDocument 52 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { AppLayout, PageHeader } from 'components' 2 | import { TokenSwapModule } from 'features/swap' 3 | import { styled } from 'junoblocks' 4 | import React from 'react' 5 | import { MigrationCard } from '../components/MigrationCard' 6 | 7 | import { APP_NAME } from '../util/constants' 8 | 9 | function getInitialTokenPairFromSearchParams() { 10 | const params = new URLSearchParams(location.search) 11 | const from = params.get('from') 12 | const to = params.get('to') 13 | return from || to ? ([from, to] as const) : undefined 14 | } 15 | 16 | export default function Home() { 17 | return ( 18 | 19 | 20 | 21 | 25 | 28 | 29 | 30 | ) 31 | } 32 | 33 | const StyledContainer = styled('div', { 34 | padding: '$4', 35 | margin: 0, 36 | }) 37 | -------------------------------------------------------------------------------- /public/bg@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/bg@1x.png -------------------------------------------------------------------------------- /public/bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/bg@2x.png -------------------------------------------------------------------------------- /public/chain_info.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "testing", 3 | "chainName": "Juno Local", 4 | "rpc": "http://localhost:26657/", 5 | "rest": "http://localhost:1317/", 6 | "stakeCurrency": { 7 | "coinDenom": "JUNO", 8 | "coinMinimalDenom": "ujuno", 9 | "coinDecimals": 6, 10 | "coinGeckoId": "juno-network" 11 | }, 12 | "bip44": { 13 | "coinType": 118 14 | }, 15 | "bech32Config": { 16 | "bech32PrefixAccAddr": "juno", 17 | "bech32PrefixAccPub": "junopub", 18 | "bech32PrefixValAddr": "junovaloper", 19 | "bech32PrefixValPub": "junovaloperpub", 20 | "bech32PrefixConsAddr": "junovalcons", 21 | "bech32PrefixConsPub": "junovalconspub" 22 | }, 23 | "currencies": [ 24 | { 25 | "coinDenom": "JUNO", 26 | "coinMinimalDenom": "ujuno", 27 | "coinDecimals": 6, 28 | "coinGeckoId": "juno-network" 29 | }, 30 | { 31 | "coinDenom": "COSM", 32 | "coinMinimalDenom": "ucosm", 33 | "coinDecimals": 6 34 | } 35 | ], 36 | "feeCurrencies": [ 37 | { 38 | "coinDenom": "JUNO", 39 | "coinMinimalDenom": "ujuno", 40 | "coinDecimals": 6, 41 | "coinGeckoId": "juno-network" 42 | } 43 | ], 44 | "coinType": 118, 45 | "gasPriceStep": { 46 | "low": 0.025, 47 | "average": 0.05, 48 | "high": 0.1 49 | }, 50 | "features": ["cosmwasm"] 51 | } 52 | -------------------------------------------------------------------------------- /public/chain_info.testnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "chainId": "uni-5", 3 | "chainName": "Juno Uni Testnet", 4 | "rpc": "https://rpc.uni.juno.deuslabs.fi:443/", 5 | "rest": "https://lcd.uni.juno.deuslabs.fi:443/", 6 | "stakeCurrency": { 7 | "coinDenom": "JUNOX", 8 | "coinMinimalDenom": "ujunox", 9 | "coinDecimals": 6 10 | }, 11 | "bip44": { 12 | "coinType": 118 13 | }, 14 | "bech32Config": { 15 | "bech32PrefixAccAddr": "juno", 16 | "bech32PrefixAccPub": "junopub", 17 | "bech32PrefixValAddr": "junovaloper", 18 | "bech32PrefixValPub": "junovaloperpub", 19 | "bech32PrefixConsAddr": "junovalcons", 20 | "bech32PrefixConsPub": "junovalconspub" 21 | }, 22 | "currencies": [ 23 | { 24 | "coinDenom": "JUNOX", 25 | "coinMinimalDenom": "ujunox", 26 | "coinDecimals": 6, 27 | "coinGeckoId": "juno-network" 28 | } 29 | ], 30 | "feeCurrencies": [ 31 | { 32 | "coinDenom": "JUNOX", 33 | "coinMinimalDenom": "ujunox", 34 | "coinDecimals": 6, 35 | "coinGeckoId": "juno-network" 36 | } 37 | ], 38 | "coinType": 118, 39 | "gasPriceStep": { 40 | "low": 0.025, 41 | "average": 0.05, 42 | "high": 0.1 43 | }, 44 | "features": ["cosmwasm"] 45 | } 46 | -------------------------------------------------------------------------------- /public/crab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/crab.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Black.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Black.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-BlackItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-BlackItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Bold.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-BoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-BoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ExtraBold.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ExtraBold.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ExtraBoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ExtraBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ExtraLight.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ExtraLight.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraLightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ExtraLightItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraLightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ExtraLightItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Italic.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Italic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Light.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Light.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-LightItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-LightItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Medium.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-MediumItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-MediumItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Regular.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-SemiBold.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-SemiBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-SemiBoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Thin.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-Thin.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ThinItalic.woff -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-ThinItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/Inter/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-BoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-ExtraBold.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-ExtraBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-ExtraBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-ExtraLight.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-ExtraLightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-ExtraLightItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-Italic.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-Light.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-LightItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-MediumItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-SemiBold.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-Thin.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono/JetBrainsMono-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/fonts/JetBrainsMono/JetBrainsMono-ThinItalic.woff2 -------------------------------------------------------------------------------- /public/ibc_assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "id": "cosmos", 5 | "name": "Atom", 6 | "symbol": "ATOM", 7 | "chain_id": "cosmoshub-4", 8 | "rpc": "https://cosmoshub.validator.network:443", 9 | "denom": "uatom", 10 | "decimals": 6, 11 | "channel": "channel-207", 12 | "juno_channel": "channel-1", 13 | "juno_denom": "ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9", 14 | "logoURI": "https://cryptologos.cc/logos/cosmos-atom-logo.svg?v=014" 15 | }, 16 | { 17 | "id": "terrausd", 18 | "name": "UST", 19 | "symbol": "UST", 20 | "chain_id": "terra-1", 21 | "rpc": "https://cosmoshub.validator.network:443", 22 | "denom": "uust", 23 | "decimals": 6, 24 | "channel": "info", 25 | "juno_channel": "channel-7", 26 | "juno_denom": "ibc/test", 27 | "logoURI": "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=014", 28 | "external_deposit_uri": "http://localhost:3000" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /public/img/keplr-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/img/keplr-icon.png -------------------------------------------------------------------------------- /public/junoswap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/junoswap.png -------------------------------------------------------------------------------- /public/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/spring-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/spring-left.png -------------------------------------------------------------------------------- /public/spring-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/spring-right.png -------------------------------------------------------------------------------- /public/springs-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CosmosContracts/junoswap-interface/f3d1567bec5c47dc748a20c2fafd15646d729f7f/public/springs-bg.png -------------------------------------------------------------------------------- /public/switch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /queries/queryMyLiquidity.ts: -------------------------------------------------------------------------------- 1 | import { queryLiquidityBalance } from '../services/liquidity' 2 | import { protectAgainstNaN } from '../util/conversion' 3 | 4 | export async function queryMyLiquidity({ swap, address, context: { client } }) { 5 | const providedLiquidityInMicroDenom = address 6 | ? await queryLiquidityBalance({ 7 | tokenAddress: swap.lp_token_address, 8 | client, 9 | address, 10 | }) 11 | : 0 12 | 13 | /* provide dollar value for reserves as well */ 14 | const totalReserve: [number, number] = [ 15 | protectAgainstNaN(swap.token1_reserve), 16 | protectAgainstNaN(swap.token2_reserve), 17 | ] 18 | 19 | const providedReserve: [number, number] = [ 20 | protectAgainstNaN( 21 | totalReserve[0] * (providedLiquidityInMicroDenom / swap.lp_token_supply) 22 | ), 23 | protectAgainstNaN( 24 | totalReserve[1] * (providedLiquidityInMicroDenom / swap.lp_token_supply) 25 | ), 26 | ] 27 | 28 | return { 29 | totalReserve, 30 | providedReserve, 31 | providedLiquidityInMicroDenom, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /queries/queryRewardsContracts.ts: -------------------------------------------------------------------------------- 1 | import { getRewardsInfo, RewardsInfoResponse } from '../services/rewards' 2 | import { convertMicroDenomToDenom } from '../util/conversion' 3 | import { InternalQueryContext } from './types' 4 | import { PoolEntityType, TokenInfoWithReward } from './usePoolsListQuery' 5 | import { PoolTokenValue } from './useQueryPools' 6 | 7 | const blocksPerSecond = 6 8 | const blocksPerYear = (525600 * 60) / blocksPerSecond 9 | 10 | export type QueryRewardsContractsArgs = { 11 | swapAddress: PoolEntityType['swap_address'] 12 | rewardsTokens: PoolEntityType['rewards_tokens'] 13 | context: InternalQueryContext 14 | } 15 | 16 | export type SerializedRewardsContract = { 17 | contract: RewardsInfoResponse 18 | tokenInfo: TokenInfoWithReward 19 | rewardRate: { 20 | ratePerBlock: PoolTokenValue 21 | ratePerYear: PoolTokenValue 22 | } 23 | } 24 | 25 | export async function queryRewardsContracts({ 26 | rewardsTokens, 27 | context: { client, getTokenDollarValue }, 28 | }: QueryRewardsContractsArgs): Promise> { 29 | const rewardsContractsInfo = await Promise.all( 30 | rewardsTokens.map(({ rewards_address }) => 31 | getRewardsInfo(rewards_address, client) 32 | ) 33 | ) 34 | 35 | const currentHeight = await client.getHeight() 36 | 37 | return await Promise.all( 38 | rewardsContractsInfo.map(async (contractInfo, index) => { 39 | const tokenInfo = rewardsTokens[index] 40 | const expired = currentHeight > contractInfo.reward.period_finish 41 | 42 | const rewardRatePerBlockInTokens = expired 43 | ? 0 44 | : convertMicroDenomToDenom( 45 | contractInfo.reward.reward_rate, 46 | tokenInfo.decimals 47 | ) 48 | 49 | const rewardRatePerBlockInDollarValue = expired 50 | ? 0 51 | : await getTokenDollarValue({ 52 | tokenInfo, 53 | tokenAmountInDenom: rewardRatePerBlockInTokens, 54 | }) 55 | 56 | const rewardRate = { 57 | ratePerBlock: { 58 | tokenAmount: rewardRatePerBlockInTokens, 59 | dollarValue: rewardRatePerBlockInDollarValue, 60 | }, 61 | ratePerYear: { 62 | tokenAmount: rewardRatePerBlockInTokens * blocksPerYear, 63 | dollarValue: rewardRatePerBlockInDollarValue * blocksPerYear, 64 | }, 65 | } 66 | 67 | return { 68 | contract: contractInfo, 69 | rewardRate, 70 | tokenInfo, 71 | } 72 | }) 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /queries/queryStakedLiquidity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getProvidedStakedAmount, 3 | getTotalStakedAmount, 4 | } from '../services/staking' 5 | import { protectAgainstNaN } from '../util/conversion' 6 | 7 | export async function queryStakedLiquidity({ 8 | address, 9 | stakingAddress, 10 | totalReserve, 11 | swap, 12 | context: { client }, 13 | }) { 14 | const [providedStakedAmountInMicroDenom, totalStakedAmountInMicroDenom] = 15 | stakingAddress 16 | ? await Promise.all([ 17 | address 18 | ? getProvidedStakedAmount(address, stakingAddress, client) 19 | : new Promise((resolve) => resolve(0)), 20 | getTotalStakedAmount(stakingAddress, client), 21 | ]) 22 | : [0, 0] 23 | 24 | const totalStakedReserve: [number, number] = [ 25 | protectAgainstNaN( 26 | totalReserve[0] * (totalStakedAmountInMicroDenom / swap.lp_token_supply) 27 | ), 28 | protectAgainstNaN( 29 | totalReserve[1] * (totalStakedAmountInMicroDenom / swap.lp_token_supply) 30 | ), 31 | ] 32 | 33 | const providedStakedReserve: [number, number] = [ 34 | protectAgainstNaN( 35 | totalReserve[0] * 36 | (providedStakedAmountInMicroDenom / swap.lp_token_supply) 37 | ), 38 | protectAgainstNaN( 39 | totalReserve[1] * 40 | (providedStakedAmountInMicroDenom / swap.lp_token_supply) 41 | ), 42 | ] 43 | 44 | return { 45 | providedStakedAmountInMicroDenom, 46 | totalStakedAmountInMicroDenom, 47 | totalStakedReserve, 48 | providedStakedReserve, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /queries/querySwapInfo.ts: -------------------------------------------------------------------------------- 1 | import { getSwapInfo } from '../services/swap' 2 | 3 | const safeNum = (attr: string | undefined): number | undefined => 4 | attr != undefined ? Number(attr) : undefined 5 | 6 | export type SwapInfoResponse = { 7 | swap_address: string 8 | lp_token_supply: number 9 | lp_token_address: string 10 | token1_denom: string 11 | token1_reserve: number 12 | token2_denom: string 13 | token2_reserve: number 14 | owner?: string 15 | lp_fee_percent?: number 16 | protocol_fee_percent?: number 17 | protocol_fee_recipient?: string 18 | } 19 | 20 | export async function querySwapInfo({ 21 | context: { client }, 22 | swap_address, 23 | }): Promise { 24 | const swap = await getSwapInfo(swap_address, client) 25 | 26 | return { 27 | ...swap, 28 | swap_address, 29 | token1_reserve: Number(swap.token1_reserve), 30 | token2_reserve: Number(swap.token2_reserve), 31 | lp_token_supply: Number(swap.lp_token_supply), 32 | protocol_fee_percent: safeNum(swap.protocol_fee_percent), 33 | lp_fee_percent: safeNum(swap.lp_fee_percent), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /queries/tokenDollarValueQuery/fetchDollarPriceByTokenIds.ts: -------------------------------------------------------------------------------- 1 | type ApiResponse = Record 2 | 3 | export const fetchDollarPriceByTokenIds = debounce( 4 | async (tokenIds: Array): Promise => { 5 | const apiIds = tokenIds.flat().join(',') 6 | 7 | const response = await fetch( 8 | `https://api.coingecko.com/api/v3/simple/price?ids=${apiIds}&vs_currencies=usd`, 9 | { 10 | method: 'GET', 11 | } 12 | ) 13 | 14 | return response.json() 15 | }, 16 | 100 17 | ) 18 | 19 | function debounce Promise>( 20 | getPromise: T, 21 | timeoutMs: number 22 | ) { 23 | let timeout 24 | let argsState = [] 25 | let resolvers = [] 26 | 27 | const debouncedPromise = (args: Parameters) => { 28 | argsState.push(args) 29 | 30 | return new Promise((resolve, reject) => { 31 | resolvers.push([resolve, reject]) 32 | 33 | clearTimeout(timeout) 34 | timeout = setTimeout(function resolvePromises() { 35 | const promise = getPromise([...argsState]) 36 | resolvers.forEach((promiseResolvers) => 37 | promise.then(...promiseResolvers) 38 | ) 39 | 40 | resolvers = [] 41 | argsState = [] 42 | }, timeoutMs) 43 | }) 44 | } 45 | 46 | return debouncedPromise as T 47 | } 48 | -------------------------------------------------------------------------------- /queries/tokenDollarValueQuery/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tokenDollarValueQuery' 2 | -------------------------------------------------------------------------------- /queries/tokenDollarValueQuery/pricingServiceIsDownAlert.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorIcon, Toast } from 'junoblocks' 2 | import React from 'react' 3 | import { toast } from 'react-hot-toast' 4 | 5 | function createAlertPricingServiceIsDown() { 6 | let hasRenderedAlert 7 | let timeout 8 | 9 | function renderAlert() { 10 | toast.custom((t) => ( 11 | } 13 | title="Oops, sorry! Our pricing service is temporarily down" 14 | onClose={() => toast.dismiss(t.id)} 15 | /> 16 | )) 17 | } 18 | 19 | return () => { 20 | if (hasRenderedAlert) { 21 | clearTimeout(timeout) 22 | timeout = setTimeout(renderAlert, 60 * 1000) 23 | return 24 | } 25 | 26 | hasRenderedAlert = true 27 | renderAlert() 28 | } 29 | } 30 | 31 | export const pricingServiceIsDownAlert = createAlertPricingServiceIsDown() 32 | -------------------------------------------------------------------------------- /queries/tokenDollarValueQuery/tokenDollarValueQuery.tsx: -------------------------------------------------------------------------------- 1 | import { TokenInfo } from 'queries/usePoolsListQuery' 2 | 3 | import { fetchDollarPriceByTokenIds } from './fetchDollarPriceByTokenIds' 4 | import { pricingServiceIsDownAlert } from './pricingServiceIsDownAlert' 5 | 6 | export async function tokenDollarValueQuery(tokenIds: Array) { 7 | if (!tokenIds?.length) { 8 | throw new Error('Provide token ids in order to query their price') 9 | } 10 | 11 | try { 12 | const prices = await fetchDollarPriceByTokenIds(tokenIds) 13 | return tokenIds.map((id): number => prices[id]?.usd || 0) 14 | } catch (e) { 15 | pricingServiceIsDownAlert() 16 | 17 | throw e 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /queries/tokenToTokenPriceQuery.ts: -------------------------------------------------------------------------------- 1 | import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate' 2 | 3 | import { 4 | getToken1ForToken2Price, 5 | getToken2ForToken1Price, 6 | getTokenForTokenPrice, 7 | } from '../services/swap' 8 | import { 9 | convertDenomToMicroDenom, 10 | convertMicroDenomToDenom, 11 | } from '../util/conversion' 12 | import { TokenInfo } from './usePoolsListQuery' 13 | import { MatchingPoolsForTokenToTokenSwap } from './useQueryMatchingPoolForSwap' 14 | 15 | type TokenToTokenPriceQueryArgs = { 16 | matchingPools: MatchingPoolsForTokenToTokenSwap 17 | tokenA: TokenInfo 18 | tokenB: TokenInfo 19 | amount: number 20 | client: CosmWasmClient 21 | } 22 | 23 | type TokenToTokenPriceQueryWithPoolsReturns = { 24 | price: number 25 | } & MatchingPoolsForTokenToTokenSwap 26 | 27 | export async function tokenToTokenPriceQueryWithPools({ 28 | matchingPools, 29 | tokenA, 30 | tokenB, 31 | amount, 32 | client, 33 | }: TokenToTokenPriceQueryArgs): Promise { 34 | if (tokenA.symbol === tokenB.symbol) { 35 | return { price: 1 } 36 | } 37 | 38 | const formatPrice = (price) => 39 | convertMicroDenomToDenom(price, tokenB.decimals) 40 | 41 | const convertedTokenAmount = convertDenomToMicroDenom(amount, tokenA.decimals) 42 | 43 | const { 44 | poolForDirectTokenAToTokenBSwap, 45 | poolForDirectTokenBToTokenASwap, 46 | passThroughPools, 47 | } = matchingPools 48 | 49 | const pricingQueries: Array> = 50 | [] 51 | 52 | if (poolForDirectTokenAToTokenBSwap) { 53 | pricingQueries.push( 54 | getToken1ForToken2Price({ 55 | nativeAmount: convertedTokenAmount, 56 | swapAddress: poolForDirectTokenAToTokenBSwap.swap_address, 57 | client, 58 | }).then((price) => ({ 59 | price: formatPrice(price), 60 | poolForDirectTokenAToTokenBSwap, 61 | })) 62 | ) 63 | } 64 | 65 | if (poolForDirectTokenBToTokenASwap) { 66 | pricingQueries.push( 67 | getToken2ForToken1Price({ 68 | tokenAmount: convertedTokenAmount, 69 | swapAddress: poolForDirectTokenBToTokenASwap.swap_address, 70 | client, 71 | }).then((price) => ({ 72 | price: formatPrice(price), 73 | poolForDirectTokenBToTokenASwap, 74 | })) 75 | ) 76 | } 77 | 78 | if (passThroughPools?.length) { 79 | passThroughPools.forEach((passThroughPool) => { 80 | pricingQueries.push( 81 | getTokenForTokenPrice({ 82 | tokenAmount: convertedTokenAmount, 83 | tokenA, 84 | tokenB, 85 | inputPool: passThroughPool.inputPool, 86 | outputPool: passThroughPool.outputPool, 87 | client, 88 | }).then((price) => ({ 89 | price: formatPrice(price), 90 | passThroughPools: [passThroughPool], 91 | })) 92 | ) 93 | }) 94 | } 95 | 96 | const prices: Array = 97 | await Promise.all(pricingQueries) 98 | 99 | /* 100 | * pick the best price among all the available swap routes. 101 | * the best price is the highest one. 102 | * */ 103 | return prices.reduce((result, tokenPrice) => { 104 | return result?.price < tokenPrice.price ? tokenPrice : result 105 | }, prices[0]) 106 | } 107 | -------------------------------------------------------------------------------- /queries/types.ts: -------------------------------------------------------------------------------- 1 | import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate' 2 | 3 | import { useGetTokenDollarValueQuery } from './useGetTokenDollarValueQuery' 4 | 5 | export type InternalQueryContext = { 6 | client: CosmWasmClient 7 | getTokenDollarValue: ReturnType[0] 8 | } 9 | -------------------------------------------------------------------------------- /queries/useGetTokenDollarValueQuery.ts: -------------------------------------------------------------------------------- 1 | import { protectAgainstNaN } from 'junoblocks' 2 | 3 | import { useCosmWasmClient } from '../hooks/useCosmWasmClient' 4 | import { useTokenDollarValue } from '../hooks/useTokenDollarValue' 5 | import { useBaseTokenInfo } from '../hooks/useTokenInfo' 6 | import { tokenToTokenPriceQueryWithPools } from './tokenToTokenPriceQuery' 7 | import { useGetQueryMatchingPoolForSwap } from './useQueryMatchingPoolForSwap' 8 | 9 | /* 10 | * takes base token price, fetches the ratio of the token provided vs the base token 11 | * and calculates the dollar value of the provided token 12 | * */ 13 | export const useGetTokenDollarValueQuery = () => { 14 | const tokenA = useBaseTokenInfo() 15 | const client = useCosmWasmClient() 16 | const [tokenADollarPrice, fetchingDollarPrice] = useTokenDollarValue( 17 | tokenA?.symbol 18 | ) 19 | 20 | const [getMatchingPoolForSwap, isLoadingPoolForSwapMatcher] = 21 | useGetQueryMatchingPoolForSwap() 22 | 23 | return [ 24 | async function getTokenDollarValue({ tokenInfo, tokenAmountInDenom }) { 25 | if (!tokenAmountInDenom) return 0 26 | 27 | const priceForOneToken = ( 28 | await tokenToTokenPriceQueryWithPools({ 29 | matchingPools: getMatchingPoolForSwap({ tokenA, tokenB: tokenInfo }), 30 | tokenA, 31 | tokenB: tokenInfo, 32 | client, 33 | amount: 1, 34 | }) 35 | )?.price 36 | 37 | return protectAgainstNaN( 38 | (tokenAmountInDenom / priceForOneToken) * tokenADollarPrice 39 | ) 40 | }, 41 | Boolean( 42 | tokenA && client && !fetchingDollarPrice && !isLoadingPoolForSwapMatcher 43 | ), 44 | ] as const 45 | } 46 | -------------------------------------------------------------------------------- /queries/usePoolsListQuery.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query' 2 | 3 | export type TokenInfo = { 4 | id: string 5 | chain_id: string 6 | token_address: string 7 | symbol: string 8 | name: string 9 | decimals: number 10 | logoURI: string 11 | tags: string[] 12 | denom: string 13 | native: boolean 14 | } 15 | 16 | export type TokenInfoWithReward = TokenInfo & { 17 | rewards_address: string 18 | } 19 | 20 | export type PoolEntityType = { 21 | pool_id: string 22 | pool_assets: [TokenInfo, TokenInfo] 23 | swap_address: string 24 | staking_address: string 25 | rewards_tokens: Array 26 | } 27 | 28 | type PoolsListQueryResponse = { 29 | base_token: TokenInfo 30 | pools: Array 31 | poolsById: Record 32 | name: string 33 | logoURI: string 34 | keywords: Array 35 | tags: Record 36 | } 37 | 38 | export const usePoolsListQuery = (options?: Parameters[1]) => { 39 | return useQuery( 40 | '@pools-list', 41 | async () => { 42 | const response = await fetch(process.env.NEXT_PUBLIC_POOLS_LIST_URL) 43 | const tokenList = await response.json() 44 | 45 | return { 46 | ...tokenList, 47 | poolsById: tokenList.pools.reduce( 48 | (poolsById, pool) => ((poolsById[pool.pool_id] = pool), poolsById), 49 | {} 50 | ), 51 | } 52 | }, 53 | Object.assign( 54 | { 55 | refetchOnMount: false, 56 | }, 57 | options || {} 58 | ) 59 | ) 60 | } 61 | 62 | export const usePoolFromListQueryById = ({ poolId }: { poolId: string }) => { 63 | const { data: poolListResponse, isLoading } = usePoolsListQuery() 64 | return [poolListResponse?.poolsById[poolId], isLoading] as const 65 | } 66 | -------------------------------------------------------------------------------- /queries/useQueryMatchingPoolForSwap/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './useQueryMatchingPoolsForSwap' 3 | -------------------------------------------------------------------------------- /queries/useQueryMatchingPoolForSwap/types.ts: -------------------------------------------------------------------------------- 1 | import { PoolEntityType } from '../usePoolsListQuery' 2 | 3 | export type PassThroughPoolsForTokenToTokenSwap = { 4 | inputPool: PoolEntityType 5 | outputPool: PoolEntityType 6 | } 7 | 8 | export type MatchingPoolsForTokenToTokenSwap = { 9 | poolForDirectTokenAToTokenBSwap?: PoolEntityType 10 | poolForDirectTokenBToTokenASwap?: PoolEntityType 11 | passThroughPools?: Array 12 | } 13 | -------------------------------------------------------------------------------- /queries/useQueryMatchingPoolForSwap/useQueryMatchingPoolsForSwap.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react' 2 | 3 | import { TokenInfo, usePoolsListQuery } from '../usePoolsListQuery' 4 | import { selectEligiblePoolsForTokenToTokenSwap } from './util/selectEligiblePoolsForTokenToTokenSwap' 5 | 6 | type GetMatchingPoolArgs = { 7 | tokenA: TokenInfo 8 | tokenB: TokenInfo 9 | } 10 | 11 | export const useGetQueryMatchingPoolForSwap = () => { 12 | const { data: poolsListResponse, isLoading } = usePoolsListQuery() 13 | 14 | const getMatchingPool = useCallback( 15 | ({ tokenA, tokenB }: GetMatchingPoolArgs) => { 16 | if (!poolsListResponse?.pools || !tokenA || !tokenB) return undefined 17 | 18 | return selectEligiblePoolsForTokenToTokenSwap({ 19 | tokenA, 20 | tokenB, 21 | poolsList: poolsListResponse.pools, 22 | }) 23 | }, 24 | [poolsListResponse] 25 | ) 26 | 27 | return [getMatchingPool, isLoading] as const 28 | } 29 | 30 | export const useQueryMatchingPoolsForSwap = ({ 31 | tokenA, 32 | tokenB, 33 | }: GetMatchingPoolArgs) => { 34 | const [getMatchingPoolsForSwap, isLoading] = useGetQueryMatchingPoolForSwap() 35 | 36 | return useMemo(() => { 37 | return [ 38 | getMatchingPoolsForSwap({ 39 | tokenA, 40 | tokenB, 41 | }), 42 | isLoading, 43 | ] as const 44 | }, [getMatchingPoolsForSwap, isLoading, tokenA, tokenB]) 45 | } 46 | -------------------------------------------------------------------------------- /queries/useQueryMatchingPoolForSwap/util/selectEligiblePoolsForTokenToTokenSwap.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BASE_TOKEN_CRAB_POOL, 3 | BASE_TOKEN_DAO_POOL, 4 | CRAB_DAO_POOL, 5 | CRAB_TOKEN, 6 | DAO_TOKEN, 7 | } from '../../../util/testutils' 8 | import { selectEligiblePoolsForTokenToTokenSwap } from './selectEligiblePoolsForTokenToTokenSwap' 9 | 10 | describe('selectEligiblePoolsForTokenToTokenSwap', () => { 11 | const poolsList = [CRAB_DAO_POOL, BASE_TOKEN_CRAB_POOL, BASE_TOKEN_DAO_POOL] 12 | 13 | it('selects token A to token B pool for swap', () => { 14 | const { poolForDirectTokenAToTokenBSwap } = 15 | selectEligiblePoolsForTokenToTokenSwap({ 16 | tokenA: CRAB_TOKEN, 17 | tokenB: DAO_TOKEN, 18 | poolsList, 19 | }) 20 | expect(poolForDirectTokenAToTokenBSwap).toBe(CRAB_DAO_POOL) 21 | }) 22 | 23 | it('selects token B to token A pool for swap', () => { 24 | const { poolForDirectTokenBToTokenASwap } = 25 | selectEligiblePoolsForTokenToTokenSwap({ 26 | tokenA: DAO_TOKEN, 27 | tokenB: CRAB_TOKEN, 28 | poolsList, 29 | }) 30 | expect(poolForDirectTokenBToTokenASwap).toBe(CRAB_DAO_POOL) 31 | }) 32 | 33 | it('selects pass through pools for swap', () => { 34 | const { passThroughPools } = selectEligiblePoolsForTokenToTokenSwap({ 35 | tokenA: CRAB_TOKEN, 36 | tokenB: DAO_TOKEN, 37 | poolsList, 38 | }) 39 | 40 | expect(passThroughPools).toEqual([ 41 | { inputPool: BASE_TOKEN_CRAB_POOL, outputPool: BASE_TOKEN_DAO_POOL }, 42 | ]) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /queries/useQueryMatchingPoolForSwap/util/selectEligiblePoolsForTokenToTokenSwap.ts: -------------------------------------------------------------------------------- 1 | import { PoolEntityType, TokenInfo } from '../../usePoolsListQuery' 2 | import { MatchingPoolsForTokenToTokenSwap } from '../types' 3 | import { validateIfPassThroughPoolMatchIsUnique } from './validateIfPassThroughPoolMatchIsUnique' 4 | 5 | type SelectEligiblePoolsForTokenToTokenSwapArgs = { 6 | tokenA: TokenInfo 7 | tokenB: TokenInfo 8 | poolsList: Array 9 | } 10 | 11 | /* find all the eligible direct and indirect swap pools */ 12 | export function selectEligiblePoolsForTokenToTokenSwap({ 13 | tokenA, 14 | tokenB, 15 | poolsList, 16 | }: SelectEligiblePoolsForTokenToTokenSwapArgs) { 17 | let eligiblePools = poolsList.reduce( 18 | (result, pool) => { 19 | const [poolAssetA, poolAssetB] = pool.pool_assets 20 | 21 | /* 22 | * validate if this pool can be used for 23 | * direct token a token b pool pair match 24 | * */ 25 | const eligibleForDirectTokenAToTokenBSwap = 26 | poolAssetA.symbol === tokenA.symbol && 27 | poolAssetB.symbol === tokenB.symbol 28 | 29 | const eligibleForDirectTokenBToTokenASwap = 30 | poolAssetA.symbol === tokenB.symbol && 31 | poolAssetB.symbol === tokenA.symbol 32 | 33 | if (eligibleForDirectTokenAToTokenBSwap) { 34 | result.poolForDirectTokenAToTokenBSwap = pool 35 | return result 36 | } 37 | 38 | if (eligibleForDirectTokenBToTokenASwap) { 39 | result.poolForDirectTokenBToTokenASwap = pool 40 | return result 41 | } 42 | 43 | /* 44 | * validate if the pool can be used as a pass through 45 | * token a token b pair match 46 | * */ 47 | const eligibleAsPassThroughInputPool = 48 | tokenA.symbol === poolAssetA.symbol || 49 | tokenA.symbol === poolAssetB.symbol 50 | 51 | if (eligibleAsPassThroughInputPool) { 52 | const intermediaryToken = 53 | tokenA.symbol === poolAssetA.symbol ? poolAssetB : poolAssetA 54 | 55 | const passThroughSwapOutputPool = poolsList.find( 56 | ({ pool_assets: [assetA, assetB] }) => { 57 | return ( 58 | (intermediaryToken.symbol === assetA.symbol && 59 | tokenB.symbol === assetB.symbol) || 60 | (tokenB.symbol === assetA.symbol && 61 | intermediaryToken.symbol === assetB.symbol) 62 | ) 63 | } 64 | ) 65 | 66 | const passThroughPoolPair = { 67 | inputPool: pool, 68 | outputPool: passThroughSwapOutputPool, 69 | } 70 | 71 | const hasEligiblePassThroughPoolPair = 72 | passThroughSwapOutputPool && 73 | validateIfPassThroughPoolMatchIsUnique( 74 | result.passThroughPools, 75 | passThroughPoolPair 76 | ) 77 | 78 | if (hasEligiblePassThroughPoolPair) { 79 | result.passThroughPools.push(passThroughPoolPair) 80 | } 81 | } 82 | 83 | return result 84 | }, 85 | { 86 | poolForDirectTokenAToTokenBSwap: null, 87 | poolForDirectTokenBToTokenASwap: null, 88 | passThroughPools: [], 89 | } as MatchingPoolsForTokenToTokenSwap 90 | ) 91 | if ( 92 | eligiblePools.poolForDirectTokenAToTokenBSwap || 93 | eligiblePools.poolForDirectTokenBToTokenASwap 94 | ) { 95 | eligiblePools.passThroughPools = [] 96 | } 97 | console.log(eligiblePools) 98 | return eligiblePools 99 | } 100 | -------------------------------------------------------------------------------- /queries/useQueryMatchingPoolForSwap/util/validateIfPassThroughPoolMatchIsUnique.ts: -------------------------------------------------------------------------------- 1 | import { PassThroughPoolsForTokenToTokenSwap } from '../types' 2 | 3 | export function validateIfPassThroughPoolMatchIsUnique( 4 | passThroughPools: Array, 5 | passThroughPoolMatch: PassThroughPoolsForTokenToTokenSwap 6 | ) { 7 | const hasPassThroughPoolMatch = passThroughPools.find((pool) => { 8 | return ( 9 | pool.inputPool.pool_id === passThroughPoolMatch.inputPool.pool_id && 10 | pool.outputPool.pool_id === passThroughPoolMatch.outputPool.pool_id 11 | ) 12 | }) 13 | 14 | return !hasPassThroughPoolMatch 15 | } 16 | -------------------------------------------------------------------------------- /queries/useQueryPoolUnstakingDuration.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query' 2 | 3 | import { useCosmWasmClient } from '../hooks/useCosmWasmClient' 4 | import { getUnstakingDuration } from '../services/staking' 5 | import { PoolEntityType, usePoolFromListQueryById } from './usePoolsListQuery' 6 | 7 | type UseQueryPoolUnstakingDurationArgs = { 8 | poolId: PoolEntityType['pool_id'] 9 | } 10 | 11 | export const useQueryPoolUnstakingDuration = ({ 12 | poolId, 13 | }: UseQueryPoolUnstakingDurationArgs) => { 14 | const [pool] = usePoolFromListQueryById({ poolId }) 15 | const client = useCosmWasmClient() 16 | return useQuery( 17 | `@unstaking-duration/${poolId}`, 18 | async () => { 19 | const seconds = await getUnstakingDuration(pool.staking_address, client) 20 | const minutes = seconds / 60 21 | const hours = minutes / 60 22 | const days = Math.round(hours / 24) 23 | return { seconds, minutes, hours, days } 24 | }, 25 | { 26 | enabled: Boolean(pool?.staking_address && client), 27 | refetchOnMount: false, 28 | } 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /services/liquidity/executeAddLiquidity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MsgExecuteContractEncodeObject, 3 | SigningCosmWasmClient, 4 | } from '@cosmjs/cosmwasm-stargate' 5 | import { coin } from '@cosmjs/stargate' 6 | 7 | import { TokenInfo } from '../../queries/usePoolsListQuery' 8 | import { 9 | createExecuteMessage, 10 | createIncreaseAllowanceMessage, 11 | validateTransactionSuccess, 12 | } from '../../util/messages' 13 | 14 | type ExecuteAddLiquidityArgs = { 15 | tokenA: TokenInfo 16 | tokenB: TokenInfo 17 | tokenAAmount: number 18 | /* 19 | * The contract calculates `tokenBAmount` automatically. 20 | * However, the user needs to set max amount of `tokenB` they're willing to spend. 21 | * If the calculated amount exceeds the max amount, the transaction then fails. 22 | */ 23 | maxTokenBAmount: number 24 | senderAddress: string 25 | swapAddress: string 26 | client: SigningCosmWasmClient 27 | } 28 | 29 | export const executeAddLiquidity = async ({ 30 | tokenA, 31 | tokenB, 32 | tokenAAmount, 33 | maxTokenBAmount, 34 | client, 35 | swapAddress, 36 | senderAddress, 37 | }: ExecuteAddLiquidityArgs): Promise => { 38 | const addLiquidityMessage = { 39 | add_liquidity: { 40 | token1_amount: `${tokenAAmount}`, 41 | max_token2: `${maxTokenBAmount}`, 42 | min_liquidity: `${0}`, 43 | }, 44 | } 45 | 46 | if (!tokenA.native || !tokenB.native) { 47 | const increaseAllowanceMessages: Array = [] 48 | 49 | /* increase allowance for each non-native token */ 50 | if (!tokenA.native) { 51 | increaseAllowanceMessages.push( 52 | createIncreaseAllowanceMessage({ 53 | tokenAmount: tokenAAmount, 54 | tokenAddress: tokenA.token_address, 55 | senderAddress, 56 | swapAddress, 57 | }) 58 | ) 59 | } 60 | if (!tokenB.native) { 61 | increaseAllowanceMessages.push( 62 | createIncreaseAllowanceMessage({ 63 | tokenAmount: maxTokenBAmount, 64 | tokenAddress: tokenB.token_address, 65 | senderAddress, 66 | swapAddress, 67 | }) 68 | ) 69 | } 70 | 71 | const executeAddLiquidityMessage = createExecuteMessage({ 72 | message: addLiquidityMessage, 73 | senderAddress, 74 | contractAddress: swapAddress, 75 | /* each native token needs to be added to the funds */ 76 | funds: [ 77 | tokenA.native && coin(tokenAAmount, tokenA.denom), 78 | tokenB.native && coin(maxTokenBAmount, tokenB.denom), 79 | ].filter(Boolean), 80 | }) 81 | 82 | return validateTransactionSuccess( 83 | await client.signAndBroadcast( 84 | senderAddress, 85 | [...increaseAllowanceMessages, executeAddLiquidityMessage], 86 | 'auto' 87 | ) 88 | ) 89 | } 90 | 91 | const funds = [ 92 | coin(tokenAAmount, tokenA.denom), 93 | coin(maxTokenBAmount, tokenB.denom), 94 | ].sort((a, b) => (a.denom > b.denom ? 1 : -1)) 95 | 96 | return await client.execute( 97 | senderAddress, 98 | swapAddress, 99 | addLiquidityMessage, 100 | 'auto', 101 | undefined, 102 | funds 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /services/liquidity/executeRemoveLiquidity.ts: -------------------------------------------------------------------------------- 1 | import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' 2 | import { 3 | createExecuteMessage, 4 | createIncreaseAllowanceMessage, 5 | validateTransactionSuccess, 6 | } from 'util/messages' 7 | 8 | type ExecuteRemoveLiquidityArgs = { 9 | tokenAmount: number 10 | senderAddress: string 11 | swapAddress: string 12 | lpTokenAddress: string 13 | client: SigningCosmWasmClient 14 | } 15 | 16 | export const executeRemoveLiquidity = async ({ 17 | tokenAmount, 18 | swapAddress, 19 | senderAddress, 20 | lpTokenAddress, 21 | client, 22 | }: ExecuteRemoveLiquidityArgs) => { 23 | const increaseAllowanceMessage = createIncreaseAllowanceMessage({ 24 | tokenAmount, 25 | senderAddress, 26 | tokenAddress: lpTokenAddress, 27 | swapAddress, 28 | }) 29 | 30 | const executeMessage = createExecuteMessage({ 31 | senderAddress, 32 | contractAddress: swapAddress, 33 | message: { 34 | remove_liquidity: { 35 | amount: `${tokenAmount}`, 36 | min_token1: `${0}`, 37 | min_token2: `${0}`, 38 | }, 39 | }, 40 | }) 41 | 42 | return validateTransactionSuccess( 43 | await client.signAndBroadcast( 44 | senderAddress, 45 | [increaseAllowanceMessage, executeMessage], 46 | 'auto' 47 | ) 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /services/liquidity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './executeAddLiquidity' 2 | export * from './executeRemoveLiquidity' 3 | export * from './queryLiquidityBalance' 4 | -------------------------------------------------------------------------------- /services/liquidity/queryLiquidityBalance.ts: -------------------------------------------------------------------------------- 1 | import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate' 2 | import { protectAgainstNaN } from 'junoblocks' 3 | 4 | type QueryLiquidityBalanceArgs = { 5 | address: string 6 | tokenAddress: string 7 | client: CosmWasmClient 8 | } 9 | 10 | export const queryLiquidityBalance = async ({ 11 | client, 12 | tokenAddress, 13 | address, 14 | }: QueryLiquidityBalanceArgs) => { 15 | try { 16 | const query = await client.queryContractSmart(tokenAddress, { 17 | balance: { address }, 18 | }) 19 | 20 | return protectAgainstNaN(Number(query.balance)) 21 | } catch (e) { 22 | console.error('Cannot get liquidity balance:', e) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /services/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from 'react-query' 2 | import { DEFAULT_REFETCH_ON_WINDOW_FOCUS_STALE_TIME } from '../util/constants' 3 | 4 | export const queryClient = new QueryClient({ 5 | defaultOptions: { 6 | queries: { 7 | refetchOnWindowFocus: true, 8 | staleTime: DEFAULT_REFETCH_ON_WINDOW_FOCUS_STALE_TIME, 9 | }, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /services/rewards.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CosmWasmClient, 3 | SigningCosmWasmClient, 4 | } from '@cosmjs/cosmwasm-stargate' 5 | 6 | import { 7 | createExecuteMessage, 8 | validateTransactionSuccess, 9 | } from '../util/messages' 10 | 11 | type Denom = 12 | | { 13 | native: string 14 | } 15 | | { 16 | /* cw20 token_address */ 17 | cw20: string 18 | } 19 | 20 | export const claimRewards = async ( 21 | senderAddress: string, 22 | rewardsAddresses: Array, 23 | client: SigningCosmWasmClient 24 | ) => { 25 | const claimRewardsMsg = { claim: {} } 26 | 27 | const messages = rewardsAddresses.map((rewardsAddress) => 28 | createExecuteMessage({ 29 | senderAddress, 30 | contractAddress: rewardsAddress, 31 | message: claimRewardsMsg, 32 | }) 33 | ) 34 | 35 | return validateTransactionSuccess( 36 | await client.signAndBroadcast(senderAddress, messages, 'auto') 37 | ) 38 | } 39 | 40 | type PendingRewardsResponse = { 41 | address: string 42 | pending_rewards: number 43 | denom: Denom 44 | } 45 | 46 | export const getPendingRewards = async ( 47 | address: string, 48 | rewardsAddress: string, 49 | client: CosmWasmClient 50 | ): Promise => { 51 | const msg = { get_pending_rewards: { address } } 52 | return await client.queryContractSmart(rewardsAddress, msg) 53 | } 54 | 55 | export type RewardsInfoResponse = { 56 | config: { 57 | owner?: string 58 | manager?: string 59 | staking_contract: string 60 | reward_token: Denom 61 | } 62 | reward: { 63 | period_finish: number 64 | reward_rate: number 65 | reward_duration: number 66 | } 67 | } 68 | 69 | export const getRewardsInfo = async ( 70 | rewardsAddress: string, 71 | client: CosmWasmClient 72 | ): Promise => { 73 | const msg = { info: {} } 74 | return await client.queryContractSmart(rewardsAddress, msg) 75 | } 76 | -------------------------------------------------------------------------------- /services/staking.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CosmWasmClient, 3 | SigningCosmWasmClient, 4 | } from '@cosmjs/cosmwasm-stargate' 5 | import { toBase64, toUtf8 } from '@cosmjs/encoding' 6 | 7 | export const stakeTokens = async ( 8 | senderAddress: string, 9 | stakeContractAddress: string, 10 | lpTokenAddress: string, 11 | amount: number, 12 | client: SigningCosmWasmClient 13 | ) => { 14 | const subMsg = { stake: {} } 15 | const encodedMsg = toBase64(toUtf8(JSON.stringify(subMsg))) 16 | const msg = { 17 | send: { 18 | contract: stakeContractAddress, 19 | amount: String(amount), 20 | msg: encodedMsg, 21 | }, 22 | } 23 | 24 | return await client.execute( 25 | senderAddress, 26 | lpTokenAddress, 27 | msg, 28 | 'auto', 29 | undefined, 30 | [] 31 | ) 32 | } 33 | 34 | export const unstakeTokens = async ( 35 | senderAddress: string, 36 | stakingContractAddress: string, 37 | amount: number, 38 | client: SigningCosmWasmClient 39 | ) => { 40 | amount = Math.floor(amount) 41 | const msg = { unstake: { amount: amount.toString() } } 42 | 43 | return await client.execute( 44 | senderAddress, 45 | stakingContractAddress, 46 | msg, 47 | 'auto', 48 | undefined, 49 | [] 50 | ) 51 | } 52 | 53 | export const claimTokens = async ( 54 | senderAddress: string, 55 | stakingContractAddress: string, 56 | client: SigningCosmWasmClient 57 | ) => { 58 | const msg = { claim: {} } 59 | return await client.execute( 60 | senderAddress, 61 | stakingContractAddress, 62 | msg, 63 | 'auto', 64 | undefined, 65 | [] 66 | ) 67 | } 68 | 69 | export const getProvidedStakedAmount = async ( 70 | address: string, 71 | stakingContractAddress: string, 72 | client: CosmWasmClient 73 | ) => { 74 | const msg = { staked_value: { address: address } } 75 | const result = await client.queryContractSmart(stakingContractAddress, msg) 76 | return Number(result.value) 77 | } 78 | 79 | export const getTotalStakedAmount = async ( 80 | stakingContractAddress: string, 81 | client: CosmWasmClient 82 | ) => { 83 | const msg = { total_value: {} } 84 | const result = await client.queryContractSmart(stakingContractAddress, msg) 85 | return Number(result.total) 86 | } 87 | 88 | export type Claim = { 89 | amount: number 90 | release_at: number 91 | } 92 | 93 | export const getClaims = async ( 94 | address: string, 95 | stakingContractAddress: string, 96 | client: CosmWasmClient 97 | ): Promise> => { 98 | const msg = { claims: { address: address } } 99 | const resp = await client.queryContractSmart(stakingContractAddress, msg) 100 | 101 | return resp.claims.map((c) => { 102 | return { 103 | amount: Number(c.amount), 104 | release_at: Number(c.release_at.at_time) / 1000000, 105 | } 106 | }) 107 | } 108 | 109 | export const getUnstakingDuration = async ( 110 | stakingContractAddress: string, 111 | client: CosmWasmClient 112 | ): Promise => { 113 | const msg = { get_config: {} } 114 | const result = await client.queryContractSmart(stakingContractAddress, msg) 115 | return result.unstaking_duration.time 116 | } 117 | -------------------------------------------------------------------------------- /services/swap/directTokenSwap.ts: -------------------------------------------------------------------------------- 1 | import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' 2 | import { coin } from '@cosmjs/stargate' 3 | 4 | import { TokenInfo } from '../../queries/usePoolsListQuery' 5 | import { 6 | createExecuteMessage, 7 | createIncreaseAllowanceMessage, 8 | validateTransactionSuccess, 9 | } from '../../util/messages' 10 | 11 | type DirectTokenSwapArgs = { 12 | swapDirection: 'tokenAtoTokenB' | 'tokenBtoTokenA' 13 | tokenAmount: number 14 | price: number 15 | slippage: number 16 | senderAddress: string 17 | swapAddress: string 18 | tokenA: TokenInfo 19 | client: SigningCosmWasmClient 20 | } 21 | 22 | export const directTokenSwap = async ({ 23 | tokenA, 24 | swapDirection, 25 | swapAddress, 26 | senderAddress, 27 | slippage, 28 | price, 29 | tokenAmount, 30 | client, 31 | }: DirectTokenSwapArgs) => { 32 | const minToken = Math.floor(price * (1 - slippage)) 33 | 34 | const swapMessage = { 35 | swap: { 36 | input_token: swapDirection === 'tokenAtoTokenB' ? 'Token1' : 'Token2', 37 | input_amount: `${tokenAmount}`, 38 | min_output: `${minToken}`, 39 | }, 40 | } 41 | 42 | if (!tokenA.native) { 43 | const increaseAllowanceMessage = createIncreaseAllowanceMessage({ 44 | senderAddress, 45 | tokenAmount, 46 | tokenAddress: tokenA.token_address, 47 | swapAddress, 48 | }) 49 | 50 | const executeMessage = createExecuteMessage({ 51 | senderAddress, 52 | contractAddress: swapAddress, 53 | message: swapMessage, 54 | }) 55 | 56 | return validateTransactionSuccess( 57 | await client.signAndBroadcast( 58 | senderAddress, 59 | [increaseAllowanceMessage, executeMessage], 60 | 'auto' 61 | ) 62 | ) 63 | } 64 | 65 | return await client.execute( 66 | senderAddress, 67 | swapAddress, 68 | swapMessage, 69 | 'auto', 70 | undefined, 71 | [coin(tokenAmount, tokenA.denom)] 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /services/swap/index.ts: -------------------------------------------------------------------------------- 1 | export * from './directTokenSwap' 2 | export * from './passThroughTokenSwap' 3 | export * from './price' 4 | -------------------------------------------------------------------------------- /services/swap/passThroughTokenSwap.ts: -------------------------------------------------------------------------------- 1 | import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' 2 | import { coin } from '@cosmjs/stargate' 3 | 4 | import { PoolEntityType, TokenInfo } from '../../queries/usePoolsListQuery' 5 | import { 6 | createExecuteMessage, 7 | createIncreaseAllowanceMessage, 8 | validateTransactionSuccess, 9 | } from '../../util/messages' 10 | 11 | type PassThroughTokenSwapArgs = { 12 | tokenAmount: number 13 | price: number 14 | slippage: number 15 | senderAddress: string 16 | inputPool: PoolEntityType 17 | outputPool: PoolEntityType 18 | tokenA: TokenInfo 19 | client: SigningCosmWasmClient 20 | } 21 | 22 | export const passThroughTokenSwap = async ({ 23 | tokenAmount, 24 | tokenA, 25 | outputPool, 26 | inputPool, 27 | senderAddress, 28 | slippage, 29 | price, 30 | client, 31 | }: PassThroughTokenSwapArgs): Promise => { 32 | const minOutputToken = Math.floor(price * (1 - slippage)) 33 | 34 | const input_token = 35 | inputPool.pool_assets[0].symbol === tokenA.symbol ? 'Token1' : 'Token2' 36 | 37 | const swapMessage = { 38 | pass_through_swap: { 39 | output_min_token: `${minOutputToken}`, 40 | input_token, 41 | input_token_amount: `${tokenAmount}`, 42 | output_amm_address: outputPool.swap_address, 43 | }, 44 | } 45 | 46 | if (!tokenA.native) { 47 | const increaseAllowanceMessage = createIncreaseAllowanceMessage({ 48 | senderAddress, 49 | tokenAmount, 50 | tokenAddress: tokenA.token_address, 51 | swapAddress: inputPool.swap_address, 52 | }) 53 | 54 | const executeMessage = createExecuteMessage({ 55 | senderAddress, 56 | contractAddress: inputPool.swap_address, 57 | message: swapMessage, 58 | }) 59 | 60 | return validateTransactionSuccess( 61 | await client.signAndBroadcast( 62 | senderAddress, 63 | [increaseAllowanceMessage, executeMessage], 64 | 'auto' 65 | ) 66 | ) 67 | } 68 | 69 | return await client.execute( 70 | senderAddress, 71 | inputPool.swap_address, 72 | swapMessage, 73 | 'auto', 74 | undefined, 75 | [coin(tokenAmount, tokenA.denom)] 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /state/atoms/transactionAtoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil' 2 | 3 | export enum TransactionStatus { 4 | IDLE = '@transaction-status/idle', 5 | EXECUTING = '@transaction-status/executing', 6 | } 7 | 8 | export const transactionStatusState = atom({ 9 | key: 'transactionState', 10 | default: TransactionStatus.IDLE, 11 | }) 12 | -------------------------------------------------------------------------------- /state/atoms/walletAtoms.ts: -------------------------------------------------------------------------------- 1 | import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' 2 | import { SigningStargateClient } from '@cosmjs/stargate' 3 | import { Key } from '@keplr-wallet/types' 4 | import { atom } from 'recoil' 5 | 6 | export enum WalletStatusType { 7 | /* nothing happens to the wallet */ 8 | idle = '@wallet-state/idle', 9 | /* restored wallets state from the cache */ 10 | restored = '@wallet-state/restored', 11 | /* the wallet is fully connected */ 12 | connected = '@wallet-state/connected', 13 | /* connecting to the wallet */ 14 | connecting = '@wallet-state/connecting', 15 | /* error when tried to connect */ 16 | error = '@wallet-state/error', 17 | } 18 | 19 | type GeneratedWalletState< 20 | TClient extends any, 21 | TStateExtension extends {} 22 | > = TStateExtension & { 23 | client: TClient | null 24 | status: WalletStatusType 25 | address: string 26 | } 27 | 28 | type CreateWalletStateArgs = { 29 | key: string 30 | default: TState 31 | } 32 | 33 | function createWalletState({ 34 | key, 35 | default: defaultState, 36 | }: CreateWalletStateArgs) { 37 | return atom>({ 38 | key, 39 | default: { 40 | status: WalletStatusType.idle, 41 | client: null, 42 | address: '', 43 | ...defaultState, 44 | }, 45 | dangerouslyAllowMutability: true, 46 | effects_UNSTABLE: [ 47 | ({ onSet, setSelf }) => { 48 | const CACHE_KEY = `@wasmswap/wallet-state/type-${key}` 49 | 50 | const savedValue = localStorage.getItem(CACHE_KEY) 51 | 52 | if (savedValue) { 53 | try { 54 | const parsedSavedState = JSON.parse(savedValue) 55 | if (parsedSavedState?.address) { 56 | setSelf({ 57 | ...parsedSavedState, 58 | client: null, 59 | status: WalletStatusType.restored, 60 | }) 61 | } 62 | } catch (e) {} 63 | } 64 | 65 | onSet((newValue, oldValue) => { 66 | const isReset = !newValue.address && (oldValue as any)?.address 67 | 68 | if (isReset) { 69 | localStorage.removeItem(CACHE_KEY) 70 | } else { 71 | localStorage.setItem( 72 | CACHE_KEY, 73 | /* let's not store the client in the cache */ 74 | JSON.stringify({ ...newValue, client: null, status: null }) 75 | ) 76 | } 77 | }) 78 | }, 79 | ], 80 | }) 81 | } 82 | 83 | export const walletState = createWalletState< 84 | SigningCosmWasmClient, 85 | { key?: Key } 86 | >({ 87 | key: 'internal-wallet', 88 | default: { 89 | key: null, 90 | }, 91 | }) 92 | 93 | export const ibcWalletState = createWalletState< 94 | SigningStargateClient, 95 | { 96 | /* ibc wallet is connected */ 97 | tokenSymbol?: string 98 | } 99 | >({ 100 | key: 'ibc-wallet', 101 | default: { 102 | tokenSymbol: null, 103 | }, 104 | }) 105 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | @import './inter.font.scss'; 2 | @import './jetbrains.font.scss'; 3 | 4 | html, 5 | body { 6 | padding: 0; 7 | margin: 0; 8 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 9 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 10 | 'Segoe UI Symbol'; 11 | min-width: 320px; 12 | width: 100%; 13 | font-size: 16px; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | text-rendering: optimizeLegibility; 17 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 18 | -webkit-tap-highlight-color: transparent; 19 | } 20 | 21 | body { 22 | background-color: #ffffff; 23 | } 24 | 25 | ::-webkit-scrollbar { 26 | width: 5px; 27 | } 28 | 29 | ::-webkit-scrollbar-thumb { 30 | background: rgba(0, 0, 0, 0.2); 31 | border-radius: 8px; 32 | } 33 | 34 | ::-webkit-scrollbar-track { 35 | background: rgba(0, 0, 0, 0); 36 | } 37 | 38 | a { 39 | color: inherit; 40 | text-decoration: none; 41 | } 42 | 43 | * { 44 | box-sizing: border-box; 45 | } 46 | 47 | input::-webkit-outer-spin-button, 48 | input::-webkit-inner-spin-button { 49 | -webkit-appearance: none; 50 | margin: 0; 51 | } 52 | 53 | /* Firefox */ 54 | input[type='number'] { 55 | -moz-appearance: textfield; 56 | } 57 | 58 | p { 59 | margin: 0; 60 | } 61 | 62 | button { 63 | text-transform: none; 64 | -webkit-appearance: button; 65 | padding: 0; 66 | border: none; 67 | outline: none; 68 | font: inherit; 69 | color: inherit; 70 | background: none; 71 | cursor: pointer; 72 | user-select: none; 73 | outline: none; 74 | } 75 | 76 | input { 77 | border: none; 78 | outline: none; 79 | display: block; 80 | width: 100%; 81 | font: inherit; 82 | color: inherit; 83 | padding: 0; 84 | background: transparent; 85 | line-height: inherit; 86 | text-align: inherit; 87 | outline: none; 88 | } 89 | 90 | [role='button'] { 91 | outline: none; 92 | } 93 | -------------------------------------------------------------------------------- /styles/inter.font.scss: -------------------------------------------------------------------------------- 1 | $InterPath: '/fonts' !default; 2 | $InterVersion: '3.19' !default; 3 | 4 | @mixin fontdef($FontType: 'Regular') { 5 | src: url('#{$InterPath}/Inter/Inter-#{$FontType}.woff2?v=#{$InterVersion}') 6 | format('woff2'), 7 | url('#{$InterPath}/Inter/Inter-#{$FontType}.woff?v=#{$InterVersion}') 8 | format('woff'); 9 | } 10 | 11 | @font-face { 12 | font-family: 'Inter'; 13 | @include fontdef('Bold'); 14 | font-weight: 700; 15 | font-style: normal; 16 | } 17 | 18 | @font-face { 19 | font-family: 'Inter'; 20 | @include fontdef('SemiBold'); 21 | font-weight: 600; 22 | font-style: normal; 23 | } 24 | 25 | @font-face { 26 | font-family: 'Inter'; 27 | @include fontdef('Medium'); 28 | font-weight: 500; 29 | font-style: normal; 30 | } 31 | 32 | @font-face { 33 | font-family: 'Inter'; 34 | @include fontdef('Regular'); 35 | font-weight: 400; 36 | font-style: normal; 37 | } 38 | -------------------------------------------------------------------------------- /styles/jetbrains.font.scss: -------------------------------------------------------------------------------- 1 | $JetBrainsMonoPath: '/fonts' !default; 2 | $JetBrainsMonoVersion: '1.101' !default; 3 | 4 | @mixin fontdef($FontType: 'Regular') { 5 | src: url('#{$JetBrainsMonoPath}/JetBrainsMono/JetBrainsMono-#{$FontType}.woff2?v=#{$JetBrainsMonoVersion}') 6 | format('woff2'); 7 | } 8 | 9 | //@font-face { 10 | // font-family: "JetBrains Mono"; 11 | // @include fontdef("Bold"); 12 | // font-weight: 600; 13 | // font-style: normal; 14 | //} 15 | 16 | @font-face { 17 | font-family: 'JetBrains Mono'; 18 | @include fontdef('Regular'); 19 | font-weight: 500; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'JetBrains Mono'; 25 | @include fontdef('Light'); 26 | font-weight: 400; 27 | font-style: normal; 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "preserveConstEnums": true, 4 | "removeComments": false, 5 | "target": "es5", 6 | "lib": ["dom", "dom.iterable", "esnext"], // List of library files to be included in the compilation 7 | "allowJs": true, // Allow JavaScript files to be compiled 8 | "skipLibCheck": true, // Skip type checking of all declaration files 9 | "esModuleInterop": true, // Disables namespace imports (import * as fs from "fs") and enables CJS/AMD/UMD style imports (import fs from "fs") 10 | "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export 11 | "strict": false, // Enable all strict type checking options 12 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file. 13 | "module": "esnext", // Specify module code generation 14 | "moduleResolution": "node", // Resolve modules using Node.js style 15 | "isolatedModules": true, // Unconditionally emit imports for unresolved files 16 | "resolveJsonModule": true, // Include modules imported with .json extension 17 | "noEmit": true, // Do not emit output (meaning do not compile code, only perform type checking) 18 | "sourceMap": true, // Generate corrresponding .map file 19 | "declaration": true, // Generate corresponding .d.ts file 20 | "noUnusedLocals": true, // Report errors on unused locals 21 | "noUnusedParameters": true, // Report errors on unused parameters 22 | "incremental": false, // Enable incremental compilation by reading/writing information from prior compilations to a file on disk 23 | "noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases in switch statement 24 | "baseUrl": ".", 25 | "jsx": "preserve", 26 | "paths": { 27 | "": ["./*"] 28 | } 29 | }, 30 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 31 | "exclude": ["node_modules"] // *** The files to not type check *** 32 | } 33 | -------------------------------------------------------------------------------- /types/window.d.ts: -------------------------------------------------------------------------------- 1 | import { Window as KeplrWindow } from '@keplr-wallet/types' 2 | 3 | declare global { 4 | // eslint-disable-next-line 5 | interface Window extends KeplrWindow {} 6 | } 7 | -------------------------------------------------------------------------------- /util/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TOKEN_BALANCE_REFETCH_INTERVAL = 15000 2 | export const DEFAULT_REFETCH_ON_WINDOW_FOCUS_STALE_TIME = 60000 // 1 minute 3 | export const SLIPPAGE_OPTIONS = [0.01, 0.02, 0.03, 0.05] 4 | export const NETWORK_FEE = 0.003 5 | export const GAS_PRICE = process.env.NEXT_PUBLIC_GAS_PRICE 6 | 7 | export const APP_NAME = process.env.NEXT_PUBLIC_SITE_TITLE 8 | export const APP_MAX_WIDTH = '1920px' 9 | 10 | export const MAIN_PANE_MAX_WIDTH = '880px' 11 | 12 | export const POOL_TOKENS_DECIMALS = 6 13 | 14 | /* the app operates in test mode */ 15 | export const __TEST_MODE__ = !JSON.parse( 16 | process.env.NEXT_PUBLIC_TEST_MODE_DISABLED 17 | ) 18 | 19 | /* feature flags */ 20 | export const __POOL_STAKING_ENABLED__ = JSON.parse( 21 | process.env.NEXT_PUBLIC_ENABLE_FEATURE_STAKING 22 | ) 23 | export const __POOL_REWARDS_ENABLED__ = JSON.parse( 24 | process.env.NEXT_PUBLIC_ENABLE_FEATURE_REWARDS 25 | ) 26 | export const __TRANSFERS_ENABLED__ = JSON.parse( 27 | process.env.NEXT_PUBLIC_ENABLE_FEATURE_TRANSFERS 28 | ) 29 | export const __DARK_MODE_ENABLED_BY_DEFAULT__ = JSON.parse( 30 | process.env.NEXT_PUBLIC_DARK_MODE_ENABLED_BY_DEFAULT 31 | ) 32 | /* /feature flags */ 33 | -------------------------------------------------------------------------------- /util/conversion/conversion.ts: -------------------------------------------------------------------------------- 1 | export const protectAgainstNaN = (value: number) => (isNaN(value) ? 0 : value) 2 | 3 | export function convertMicroDenomToDenom( 4 | value: number | string, 5 | decimals: number 6 | ): number { 7 | if (decimals === 0) return Number(value) 8 | 9 | return protectAgainstNaN(Number(value) / Math.pow(10, decimals)) 10 | } 11 | 12 | export function convertDenomToMicroDenom( 13 | value: number | string, 14 | decimals: number 15 | ): number { 16 | if (decimals === 0) return Number(value) 17 | 18 | return protectAgainstNaN( 19 | parseInt(String(Number(value) * Math.pow(10, decimals)), 10) 20 | ) 21 | } 22 | 23 | export function convertFromMicroDenom(denom: string) { 24 | return denom?.substring(1).toUpperCase() 25 | } 26 | 27 | export function convertToFixedDecimals(value: number | string): string { 28 | const amount = Number(value) 29 | return amount > 0.01 ? amount.toFixed(2) : String(amount) 30 | } 31 | 32 | export const formatTokenName = (name: string) => { 33 | if (name) { 34 | return name.slice(0, 1).toUpperCase() + name.slice(1).toLowerCase() 35 | } 36 | return '' 37 | } 38 | -------------------------------------------------------------------------------- /util/conversion/index.ts: -------------------------------------------------------------------------------- 1 | export * from './conversion' 2 | export * from './pools' 3 | -------------------------------------------------------------------------------- /util/conversion/pools.ts: -------------------------------------------------------------------------------- 1 | import { POOL_TOKENS_DECIMALS } from '../constants' 2 | import { convertMicroDenomToDenom } from './conversion' 3 | 4 | export const calcPoolTokenValue = ({ 5 | tokenAmountInMicroDenom, 6 | tokenSupply, 7 | tokenReserves, 8 | }) => { 9 | return convertMicroDenomToDenom( 10 | (tokenAmountInMicroDenom / tokenSupply) * tokenReserves, 11 | POOL_TOKENS_DECIMALS 12 | ) 13 | } 14 | 15 | export const calcPoolTokenDollarValue = ({ 16 | tokenAmountInMicroDenom, 17 | tokenSupply, 18 | tokenReserves, 19 | tokenDollarPrice, 20 | }) => { 21 | return ( 22 | calcPoolTokenValue({ 23 | tokenAmountInMicroDenom, 24 | tokenSupply, 25 | tokenReserves, 26 | }) * 27 | tokenDollarPrice * 28 | 2 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /util/cosmWasmClientRouter.ts: -------------------------------------------------------------------------------- 1 | import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate' 2 | 3 | type CosmWasmClients = { 4 | [rpcEndpoint: string]: CosmWasmClient 5 | } 6 | 7 | class CosmWasmClientRouter { 8 | clients: CosmWasmClients = {} 9 | 10 | async connect(rpcEndpoint) { 11 | if (!this.getClientInstance(rpcEndpoint)) { 12 | this.setClientInstance(rpcEndpoint, CosmWasmClient.connect(rpcEndpoint)) 13 | } 14 | 15 | return this.getClientInstance(rpcEndpoint) 16 | } 17 | 18 | getClientInstance(rpcEndpoint) { 19 | return this.clients[rpcEndpoint] 20 | } 21 | setClientInstance(rpcEndpoint, client) { 22 | this.clients[rpcEndpoint] = client 23 | } 24 | } 25 | 26 | export const cosmWasmClientRouter = new CosmWasmClientRouter() 27 | -------------------------------------------------------------------------------- /util/externalLinkPopup.ts: -------------------------------------------------------------------------------- 1 | type ExternalLinkPopupArgs = { 2 | url: string 3 | title: string 4 | width: number 5 | height: number 6 | } 7 | 8 | export function externalLinkPopup({ 9 | url, 10 | title, 11 | width: w, 12 | height: h, 13 | }: ExternalLinkPopupArgs) { 14 | const dualScreenLeft = 15 | window.screenLeft !== undefined ? window.screenLeft : window.screenX 16 | const dualScreenTop = 17 | window.screenTop !== undefined ? window.screenTop : window.screenY 18 | 19 | const width = window.innerWidth 20 | ? window.innerWidth 21 | : document.documentElement.clientWidth 22 | ? document.documentElement.clientWidth 23 | : screen.width 24 | const height = window.innerHeight 25 | ? window.innerHeight 26 | : document.documentElement.clientHeight 27 | ? document.documentElement.clientHeight 28 | : screen.height 29 | 30 | const systemZoom = width / window.screen.availWidth 31 | const left = (width - w) / 2 / systemZoom + dualScreenLeft 32 | const top = (height - h) / 2 / systemZoom + dualScreenTop 33 | const newWindow = window.open( 34 | url, 35 | title, 36 | ` 37 | scrollbars=yes, 38 | width=${w / systemZoom}, 39 | height=${h / systemZoom}, 40 | top=${top}, 41 | left=${left}, 42 | location=yes, 43 | status=yes 44 | ` 45 | ) 46 | 47 | if (window.focus) newWindow.focus() 48 | 49 | return newWindow 50 | } 51 | 52 | export function externalLinkPopupAutoWidth( 53 | args: Omit 54 | ) { 55 | const width = Math.max(450, Math.round(window.innerWidth * 0.35)) 56 | const height = Math.max(300, Math.round(window.innerHeight * 0.8)) 57 | return externalLinkPopup({ 58 | ...args, 59 | width, 60 | height, 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /util/formatCompactNumber.ts: -------------------------------------------------------------------------------- 1 | import { dollarValueFormatter, formatTokenBalance } from 'junoblocks' 2 | 3 | const formatWithOneDecimal = new Intl.NumberFormat('en-US', { 4 | minimumFractionDigits: 0, 5 | maximumFractionDigits: 1, 6 | }) 7 | 8 | const oneMillion = 1e6 9 | 10 | export const formatCompactNumber = ( 11 | value: number, 12 | kind: 'tokenAmount' | 'dollarValue' = 'dollarValue' 13 | ) => { 14 | if (value > oneMillion) { 15 | return `${formatWithOneDecimal.format(value / oneMillion)}M` 16 | } 17 | if (value > 10000) { 18 | return `${formatWithOneDecimal.format(Math.round(value / 1000))}K` 19 | } 20 | if (value > 1000) { 21 | return dollarValueFormatter(Math.round(value), { 22 | includeCommaSeparation: true, 23 | }) 24 | } 25 | if (kind === 'dollarValue') { 26 | return dollarValueFormatter(value, { 27 | includeCommaSeparation: true, 28 | }) 29 | } 30 | return formatTokenBalance(value) 31 | } 32 | 33 | export const formatCurrency = ( 34 | value: number, 35 | options?: Intl.NumberFormatOptions 36 | ) => { 37 | return Intl.NumberFormat('en-US', { 38 | style: 'currency', 39 | currency: 'USD', 40 | ...options, 41 | }).format(value) 42 | } 43 | -------------------------------------------------------------------------------- /util/formatSdkErrorMessage.ts: -------------------------------------------------------------------------------- 1 | export function formatSdkErrorMessage(e) { 2 | return String(e).length > 300 3 | ? `${String(e).substring(0, 150)} ... ${String(e).substring( 4 | String(e).length - 150 5 | )}` 6 | : String(e) 7 | } 8 | -------------------------------------------------------------------------------- /util/getPropsForInteractiveElement.ts: -------------------------------------------------------------------------------- 1 | import { KeyboardEvent, MouseEvent } from 'react' 2 | 3 | type Args = { 4 | onClick: (event: MouseEvent | KeyboardEvent) => void 5 | } 6 | 7 | export const getPropsForInteractiveElement = ({ onClick }: Args) => ({ 8 | role: 'button', 9 | onClick, 10 | onKeyDown(event: KeyboardEvent) { 11 | const { code } = event 12 | const shouldToggleSelector = ['space', 'enter'].includes(code.toLowerCase()) 13 | if (shouldToggleSelector) { 14 | onClick(event) 15 | } 16 | }, 17 | tabIndex: 0, 18 | }) 19 | -------------------------------------------------------------------------------- /util/localStorageEffect.ts: -------------------------------------------------------------------------------- 1 | export const localStorageEffect = 2 | (key) => 3 | ({ setSelf, onSet }) => { 4 | if (typeof window !== 'undefined') { 5 | const savedValue = localStorage.getItem(key) 6 | if (savedValue != null) { 7 | setSelf(JSON.parse(savedValue)) 8 | } 9 | 10 | onSet((newValue, _, isReset) => { 11 | isReset 12 | ? localStorage.removeItem(key) 13 | : localStorage.setItem(key, JSON.stringify(newValue)) 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /util/maybePluralize.ts: -------------------------------------------------------------------------------- 1 | export const maybePluralize = (count: number, noun: string, suffix = 's') => 2 | `${count} ${noun}${count !== 1 ? suffix : ''}` 3 | -------------------------------------------------------------------------------- /util/messages/createExecuteMessage.ts: -------------------------------------------------------------------------------- 1 | import { MsgExecuteContractEncodeObject } from '@cosmjs/cosmwasm-stargate' 2 | import { toUtf8 } from '@cosmjs/encoding' 3 | import { Coin } from '@cosmjs/launchpad' 4 | import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' 5 | 6 | type CreateExecuteMessageArgs = { 7 | senderAddress: string 8 | message: Record> 9 | contractAddress: string 10 | funds?: Array 11 | } 12 | 13 | export const createExecuteMessage = ({ 14 | senderAddress, 15 | contractAddress, 16 | message, 17 | funds, 18 | }: CreateExecuteMessageArgs): MsgExecuteContractEncodeObject => ({ 19 | typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', 20 | value: MsgExecuteContract.fromPartial({ 21 | sender: senderAddress, 22 | contract: contractAddress, 23 | msg: toUtf8(JSON.stringify(message)), 24 | funds: funds || [], 25 | }), 26 | }) 27 | -------------------------------------------------------------------------------- /util/messages/createIncreaseAllowanceMessage.ts: -------------------------------------------------------------------------------- 1 | import { MsgExecuteContractEncodeObject } from '@cosmjs/cosmwasm-stargate' 2 | import { toUtf8 } from '@cosmjs/encoding' 3 | import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' 4 | 5 | type CreateIncreaseAllowanceMessageArgs = { 6 | senderAddress: string 7 | tokenAmount: number 8 | tokenAddress: string 9 | swapAddress: string 10 | } 11 | 12 | export const createIncreaseAllowanceMessage = ({ 13 | senderAddress, 14 | tokenAmount, 15 | tokenAddress, 16 | swapAddress, 17 | }: CreateIncreaseAllowanceMessageArgs): MsgExecuteContractEncodeObject => ({ 18 | typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', 19 | value: MsgExecuteContract.fromPartial({ 20 | sender: senderAddress, 21 | contract: tokenAddress, 22 | msg: toUtf8( 23 | JSON.stringify({ 24 | increase_allowance: { 25 | amount: `${tokenAmount}`, 26 | spender: `${swapAddress}`, 27 | }, 28 | }) 29 | ), 30 | funds: [], 31 | }), 32 | }) 33 | -------------------------------------------------------------------------------- /util/messages/index.ts: -------------------------------------------------------------------------------- 1 | export { createExecuteMessage } from './createExecuteMessage' 2 | export { createIncreaseAllowanceMessage } from './createIncreaseAllowanceMessage' 3 | export { validateTransactionSuccess } from './validateTransactionSuccess' 4 | -------------------------------------------------------------------------------- /util/messages/validateTransactionSuccess.ts: -------------------------------------------------------------------------------- 1 | import { DeliverTxResponse, isDeliverTxFailure } from '@cosmjs/stargate' 2 | 3 | export function validateTransactionSuccess(result: DeliverTxResponse) { 4 | if (isDeliverTxFailure(result)) { 5 | throw new Error( 6 | `Error when broadcasting tx ${result.transactionHash} at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}` 7 | ) 8 | } 9 | 10 | return result 11 | } 12 | -------------------------------------------------------------------------------- /util/testutils.ts: -------------------------------------------------------------------------------- 1 | import { PoolEntityType, TokenInfo } from '../queries/usePoolsListQuery' 2 | 3 | export const BASE_TOKEN: TokenInfo = { 4 | id: '', 5 | chain_id: 'lucina', 6 | token_address: '', 7 | symbol: 'WSWAP', 8 | name: 'Wasmswap', 9 | decimals: 6, 10 | logoURI: 'https://cryptologos.cc/logos/anyswap-any-logo.svg?v=022', 11 | tags: ['native'], 12 | native: true, 13 | denom: 'ujuno', 14 | } 15 | 16 | export const CRAB_TOKEN: TokenInfo = { 17 | id: '', 18 | chain_id: 'lucina', 19 | token_address: 20 | 'juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8', 21 | symbol: 'CRAB', 22 | name: 'Crab', 23 | decimals: 6, 24 | logoURI: 'https://cryptologos.cc/logos/irisnet-iris-logo.svg?v=022', 25 | tags: ['memecoin'], 26 | native: false, 27 | denom: 'ucrab', 28 | } 29 | 30 | export const DAO_TOKEN: TokenInfo = { 31 | id: '', 32 | chain_id: 'lucina', 33 | token_address: 34 | 'juno1yw4xvtc43me9scqfr2jr2gzvcxd3a9y4eq7gaukreugw2yd2f8ts9z8cq8', 35 | symbol: 'DAO', 36 | name: 'DAO', 37 | decimals: 6, 38 | logoURI: 'https://cryptologos.cc/logos/torn-torn-logo.svg?v=022', 39 | tags: ['memecoin'], 40 | native: false, 41 | denom: 'udao', 42 | } 43 | 44 | export const BASE_TOKEN_CRAB_POOL: PoolEntityType = { 45 | pool_id: 'WSWAP-CRAB', 46 | pool_assets: [BASE_TOKEN, CRAB_TOKEN], 47 | swap_address: 48 | 'juno1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq68ev2p', 49 | staking_address: 50 | 'juno16y344e8ryydmeu2g8yyfznq79j7jfnar4p59ngpvaazcj83jzsmsnvh4g8', 51 | rewards_tokens: [], 52 | } 53 | 54 | export const BASE_TOKEN_DAO_POOL: PoolEntityType = { 55 | pool_id: 'WSWAP-DAO', 56 | pool_assets: [BASE_TOKEN, DAO_TOKEN], 57 | swap_address: 58 | 'juno1hulx7cgvpfcvg83wk5h96sedqgn72n026w6nl47uht554xhvj9ns263mx9', 59 | staking_address: 60 | 'juno1q23d30x94cm8ve243pxdc52m398r4l5ecgcfp8tud3vggcsq8s2qv7kt2z', 61 | rewards_tokens: [], 62 | } 63 | 64 | export const CRAB_DAO_POOL: PoolEntityType = { 65 | pool_id: 'CRAB-DAO', 66 | pool_assets: [CRAB_TOKEN, DAO_TOKEN], 67 | swap_address: 68 | 'juno187zds75uenfxht2zqz7e0wxn3ushcawvf2ndrns6q63hgfn6ptqq7d7smp', 69 | staking_address: '', 70 | rewards_tokens: [], 71 | } 72 | -------------------------------------------------------------------------------- /webpack.functions.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('webpack-node-externals') 2 | 3 | module.exports = { 4 | externals: [nodeExternals()], 5 | } 6 | --------------------------------------------------------------------------------