From 8f9c72dbc41d6d5603e5307e7a4847afc155a9ea Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Wed, 9 Nov 2022 10:26:27 -0600 Subject: [PATCH] Publish redesign (#978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New assets page * Fixes from review * WIP sidebar design * WIP pool quick actions * Tweak banner and add event listeners * Remove non-external data from external query store * Fix external query ref * Fix memoization issues * Refactor + add "add liquidity" quick action * Move toasts * Add quick actions to external incentives table * Add remove liquidity + filter options for user-relevance * Fix quick action menu filter * Add superfluid config, naming * Only show remove liquidity if liquidity available * Add quick lock callbacks * Update .gitignore * Add pool detail and gauge wrapper hooks * Add superfluid modal to quick action * Remove logs * Tweaks for assets page * Fix ref error * Rename pool detail and SFS pool store -> config * Remove highcharts from bundle * Fix nav bar button reset bug * Overload nav bar hook * WIP new pool page (header) * Create pool, step 1 * Progress on create pool modal * Responsive pool page overview graphic * Filter pool dust via user setting * Store dust setting in local storage * Filter assets * Filter pools in assets page * Fix nav bar store * Fix error state + button disabled state * Update step 2 * Step 3 create pool * Trade tokens modal * Remove primary 200 * Use name in nav bar instead of address * WIP new pool detail page * Accumulate prior internal incentives * Move superfluid info * Add superfluid UI * Go superfluid * Add unlock token functionality WIP * Tweak drawer * Set up add and remove liquidity modals, remove manage liquidity modal * WIP new remove liquidity * New slider * Update remove liquidity * Fix build errors * Fix swap fee apy * Update display of APRs + refactor external query store * Add swap fee row * Fix drawer positioning * Increase height of drawer * Add vial images * Use memoization * Add updated "go superfluid" * Handle go superfluid * Move details button on drawer expansion * Add fallback img for sfs validators * Tweak drawer * WIP update lock tokens modal * remove shadows * Update lock tokens modal w/ apr * Remove max button * Update add liquidity modal * Show shares in remove liquidity * Fix react warning * Fix unlock bug * Fix color * Tweak create pool UI * Show unbonding shares * Localization (#899) * Localization initialization * Add formatted component to translate with inside component * Add translations for 404 and 505 page * Add menu translation, update formatted component for
component * Add translations of trade-clipboard * Add translations of pools * Add translations of pool * Add translation of assets * Add translations for toasts * Move error translations to root of json file * Review of french translations * Add language setting and language-select * Remove unnecessary comments * fix menu dropdown icon * refractor icon dropdown * Add icon dropdown for mobile * Add method getLabel in User setting to get translated label * fix dropdown label, fix dependency of create pool button * Add translation for pool creation * Add documentation for Formatted * add t in useMemo dependencies * remove unused comments * refractor ShowDust constructor * Tweak * Fix errors from merge * Add pool catalyst view * Fix drawer * Add user stats * Add user stats view WIP * Update gauge APR calculation * APY -> APR * WIP sidebar and pool detail page * WIP mobile pool detail page - level 1 * Mobile navbar WIP * Progress on mobile nav * Pools page and pool detail page WIP * Fix pool detail * Pools page * Update tab text * Asset table * Tweak assets page * New pre-transfer modal * Fix build errors * Add fiat on ramps (#924) * Add transak modal * Change buy button name * Add kado * Fix asset table button appearances * Fix transfer * Update kado logo * Hardcode non-secret API key * Merge remote-tracking branch 'origin/feat/redesign' into jonator/redesign/fiat-onramps * Fix transfer config * Fix transfer config * Update API keys with KYB enabled * Fix deposit click event * Update buttons * Fix null check * Fix connect wallet buttons * Fix padding * Mobile remove and add liquidity * Update pool page with create pool at bottom * Update to Kevin's recommended preview image * Add pool rewards query * Fix daily reward query * Fix formatting * Fix metric being displayed * User stats on mobile + missing title * Update pool detail page translations * Translate errors, cleanup * Lock tokens modal tx * More translations * Create pool + go superfluid * Fix liquidity buttons on various screen sizes * More tweaks for localization and lock token modal * Fix mobile nav bar + menu * Tweaks * Update colors and tooltip + update icons * Add translations (#947) * Add translations * Fixes * fix typo Co-authored-by: Jon Ator * Fix translations * Fix incentive drawer text wrap + rem log * Mobile redesign + fixes (#937) * WIP sidebar and pool detail page * WIP mobile pool detail page - level 1 * Mobile navbar WIP * Progress on mobile nav * Pools page and pool detail page WIP * Fix pool detail * Pools page * Update tab text * Asset table * Tweak assets page * New pre-transfer modal * Fix build errors * Update buttons * Fix null check * Fix connect wallet buttons * Fix padding * Mobile remove and add liquidity * Update pool page with create pool at bottom * Update to Kevin's recommended preview image * Add pool rewards query * Fix daily reward query * Fix formatting * Fix metric being displayed * User stats on mobile + missing title * Update pool detail page translations * Translate errors, cleanup * Lock tokens modal tx * More translations * Create pool + go superfluid * Fix liquidity buttons on various screen sizes * More tweaks for localization and lock token modal * Fix mobile nav bar + menu * Tweaks * Update colors and tooltip + update icons * Add translations (#947) * Add translations * Fixes * fix typo Co-authored-by: Jon Ator * Fix translations Co-authored-by: Dorian Latouche * Don't show on ramps on mobile * Add missing translations * update fr translations (#949) * Updates * Remove space * Remove old button component * Remove old colors, update sfs modal * Update trade clipboard WIP * Trade clipboard mobile * Always use token select modal * Update colors in pool token select * Remove bg-card * Remove bg-card 2 * Fix colors * CSS fixes * Update menu label, add missing translations * Tweak trade clipboard * Tweaks and add learn more link * Disable button * Text over graphic * Update readme, remove unused code * Tweaks * Adjust modal background * Transfer modal fixes * Fix address display issue * Fix from address logic * Colors * Add insufficient balance error * Add error message to create pool * Add superfluid APR, remove log * save settings in local storage in UserSetting (#951) * Adjust shares on mobile/ handle overflow * Add feature flag for transak * Mobile- reduce size * Use configured base urls * add frontier colors - remove unused gradients * Update superfluid color * add korean WIP (#953) * Remove language icons, use native language in menu * Add levels to bond liquidity config * Subtract the sfs APR * Comment * Declare type explicitly * Added Polish translation (#960) * Added a polish translation * Minor fixes * Removed language names from file * Use cloudflare price middleware * Español esta aqui amigos (#962) * Español esta aqui amigos * Add Español to dropdown * Some fixes * Some fixes * Some fixes * Turkish Language Added Turkish crypto community is one of the largest in crypto. We felt the need to add Turkish as a language to Osmosis. * Superfulid text, wallet info button * Left align transfer table text * Animate pool card hover * Fix superfluid texts * Reduce size of * Center switch icon * Manual slippage bg * Fix turkish * Button height and color transition * Use normal position for token select * Various fixes * Fix quick action dropdown location, close others * Fix menu dropdowns, use border button swap tool * Fixes + improvements * Fixes * Use coingecko * Update bond card arrangement * Fix bond card backdrop * Revert "Use coingecko" This reverts commit 46e2cd0292651edb955fd45a41b1a815162e0c6d. * Fix pool quick action bug * Auto set superfluid * Frontier button black text * Black -> osmoverse * Update nav bar, buttons, tablet * Tweak asset table controls * Input box animation on focus * Caret color * Trade page bg * Lock token menu, make sidebar text AX * Dropdown hover state * Remove selected language from dropdown * Pool detail gap * Tweaks * Close quick action modal on tx complete * Lock tokens on mobile modal tweak * Lock token modal - mobile * Add arrow button * Fix quick action modal titles * Update bond card * Tweak table spacing * remove scrollbar style * Match button sizes * Pool table control spacing + focus effect * Update Spanish (#973) * Spanish Fixes * Fix spanish * Spanish fixes * Small fixes * Fix button style * Translate wrap asset link * Fix external links * UI for no bonding periods * Tweaks on mobile pool detail page * Tablet pool detail tweaks * Mobile tweaks * Tweak nav bar spacing * Tweak search bar offset * Remove white background * Add files via upload (#981) -Fixed missing lines. -Removed extra lines. Co-authored-by: Dorian Latouche Co-authored-by: KIM, WOOJUNG Co-authored-by: pawarm Co-authored-by: Daniel Farina Co-authored-by: Huginn.Academy <107777584+HuginnAcademy@users.noreply.github.com> --- .gitignore | 1 + CODEOWNERS | 1 - Dockerfile | 20 + README.md | 2 + docker-compose.yml | 9 + packages/stores/src/price/pool.ts | 4 +- packages/stores/src/queries-external/base.ts | 13 + packages/stores/src/queries-external/index.ts | 6 + .../src/queries-external/pool-fees/index.ts | 63 +- .../src/queries-external/pool-fees/types.ts | 11 +- .../queries-external/pool-rewards/index.ts | 70 + .../queries-external/pool-rewards/types.ts | 20 + packages/stores/src/queries-external/store.ts | 41 +- .../pool-incentives/incentivized-pools.ts | 119 +- .../stores/src/queries/pools/pool-details.ts | 51 +- .../src/queries/superfluid-pools/pool.ts | 41 +- packages/stores/src/ui-config/create-pool.ts | 57 +- packages/stores/src/ui-config/errors.ts | 97 + packages/stores/src/ui-config/index.ts | 4 +- .../manage-liquidity/add-liquidity.ts | 9 +- .../manage-liquidity/bond-liquidity.ts | 255 ++ .../src/ui-config/manage-liquidity/errors.ts | 23 + .../src/ui-config/manage-liquidity/index.ts | 1 + .../manage-liquidity/remove-liquidity.ts | 25 +- .../stores/src/ui-config/slippage-config.ts | 11 +- .../src/ui-config/trade-token-in-config.ts | 11 +- packages/stores/src/ui-config/user-config.ts | 18 + .../stores/types/queries-external/base.d.ts | 5 + .../stores/types/queries-external/index.d.ts | 3 + .../queries-external/pool-fees/index.d.ts | 11 +- .../queries-external/pool-fees/types.d.ts | 10 +- .../queries-external/pool-rewards/index.d.ts | 18 + .../queries-external/pool-rewards/types.d.ts | 18 + .../stores/types/queries-external/store.d.ts | 16 +- .../pool-incentives/incentivized-pools.d.ts | 11 +- .../types/queries/pools/pool-details.d.ts | 16 +- .../types/queries/superfluid-pools/pool.d.ts | 19 +- .../stores/types/ui-config/create-pool.d.ts | 4 +- packages/stores/types/ui-config/errors.d.ts | 37 + packages/stores/types/ui-config/index.d.ts | 3 +- .../manage-liquidity/bond-liquidity.d.ts | 59 + .../ui-config/manage-liquidity/errors.d.ts | 9 + .../ui-config/manage-liquidity/index.d.ts | 1 + .../manage-liquidity/remove-liquidity.d.ts | 5 +- .../types/ui-config/slippage-config.d.ts | 2 +- .../stores/types/ui-config/user-config.d.ts | 6 + packages/web/components/alert/error.tsx | 24 - packages/web/components/alert/index.ts | 1 - packages/web/components/alert/info.tsx | 44 +- packages/web/components/alert/temp-banner.tsx | 9 +- packages/web/components/alert/toast.tsx | 112 +- .../web/components/animation/bridge/index.tsx | 11 +- .../web/components/assets/catalyst-icon.tsx | 1 - .../web/components/assets/fallback-img.tsx | 23 + .../web/components/assets/gradient-view.tsx | 2 +- packages/web/components/assets/index.tsx | 1 + .../components/assets/pool-assets-icon.tsx | 30 +- .../components/assets/pool-assets-name.tsx | 6 +- packages/web/components/assets/token.tsx | 11 +- .../web/components/buttons/arrow-button.tsx | 23 + .../web/components/buttons/border-button.tsx | 19 + packages/web/components/buttons/button.tsx | 131 +- .../web/components/buttons/close-button.tsx | 2 +- .../web/components/buttons/icon-button.tsx | 43 - packages/web/components/buttons/index.ts | 3 +- packages/web/components/buttons/show-more.tsx | 44 +- .../web/components/buttons/switch-wallet.tsx | 2 +- packages/web/components/cards/asset-card.tsx | 92 +- .../cards/{wallet.tsx => asset-source.tsx} | 6 +- packages/web/components/cards/base.tsx | 120 - packages/web/components/cards/bond-card.tsx | 405 +++ .../web/components/cards/go-superfluid.tsx | 30 - .../components/cards/incentivized-pool.tsx | 53 - packages/web/components/cards/index.tsx | 11 +- packages/web/components/cards/my-pool.tsx | 75 - packages/web/components/cards/pool-card.tsx | 155 +- .../web/components/cards/pool-catalyst.tsx | 70 - .../web/components/cards/pool-gauge-bonus.tsx | 38 - packages/web/components/cards/pool-gauge.tsx | 53 - .../components/cards/superfluid-validator.tsx | 65 - .../web/components/chart/asset-breakdown.tsx | 72 + .../web/components/chart/generate-series.ts | 2 +- packages/web/components/chart/gradients.ts | 2 +- packages/web/components/chart/index.ts | 2 + packages/web/components/chart/pie-chart.tsx | 29 +- .../web/components/chart/price-breakdown.tsx | 70 + .../web/components/complex/add-liquidity.tsx | 216 ++ .../complex/all-pools-table-set.tsx | 1034 +++--- .../complex/compact-pool-table-display.tsx | 33 +- .../external-incentivized-pools-table-set.tsx | 133 +- packages/web/components/complex/index.ts | 1 - .../complex/pool/create/step-base.tsx | 95 +- .../complex/pool/create/step1-set-ratios.tsx | 47 +- .../pool/create/step2-add-liquidity.tsx | 53 +- .../complex/pool/create/step3-confirm.tsx | 58 +- .../components/complex/pool/create/types.ts | 1 - .../components/complex/remove-liquidity.tsx | 111 + .../web/components/complex/sidebar-bottom.tsx | 251 -- packages/web/components/complex/transfer.tsx | 69 +- packages/web/components/control/checkbox.tsx | 8 +- .../control/icon-dropdown/icon-dropdown.tsx | 87 + .../components/control/icon-dropdown/index.ts | 3 + .../icon-dropdown/menu-dropdown-icon-item.tsx | 51 + .../icon-dropdown/menu-dropdown-icon.tsx | 44 + packages/web/components/control/index.ts | 3 + .../components/control/language-select.tsx | 40 + .../web/components/control/menu-dropdown.tsx | 26 +- .../web/components/control/menu-toggle.tsx | 14 +- packages/web/components/control/page-list.tsx | 4 +- .../components/control/pool-token-select.tsx | 12 +- packages/web/components/control/radio.tsx | 8 +- packages/web/components/control/slider.tsx | 131 +- packages/web/components/control/sort-menu.tsx | 57 +- packages/web/components/control/switch.tsx | 41 +- packages/web/components/control/tab-box.tsx | 13 +- packages/web/components/control/toggle.tsx | 10 +- .../web/components/control/token-select.tsx | 170 +- packages/web/components/control/types.ts | 6 + packages/web/components/input/input-box.tsx | 24 +- packages/web/components/input/search-box.tsx | 73 +- packages/web/components/layouts/main.tsx | 253 +- packages/web/components/left-time.tsx | 51 - .../web/components/localization/formatted.tsx | 245 ++ packages/web/components/localization/index.ts | 2 + .../localization/translate-error.ts | 72 + packages/web/components/main-menu.tsx | 123 + packages/web/components/navbar/index.tsx | 248 ++ packages/web/components/ogp-meta.tsx | 21 +- packages/web/components/overview/index.tsx | 110 - .../overview/overview-label-value.tsx | 60 - packages/web/components/overview/pools.tsx | 109 + packages/web/components/pixels/pallete.tsx | 4 +- .../web/components/table/assets-table.tsx | 539 +-- .../web/components/table/cells/asset-name.tsx | 6 +- .../web/components/table/cells/balance.tsx | 4 +- packages/web/components/table/cells/index.ts | 1 + .../table/cells/pool-composition.tsx | 40 +- .../table/cells/pool-quick-actions.tsx | 126 + .../table/cells/transfer-button.tsx | 83 +- packages/web/components/table/cells/types.ts | 1 + .../components/table/cells/validator-info.tsx | 2 +- .../web/components/table/depooling-table.tsx | 17 +- packages/web/components/table/index.tsx | 44 +- .../web/components/table/transfer-history.tsx | 30 +- packages/web/components/table/types.ts | 2 +- packages/web/components/tooltip/info.tsx | 20 +- .../web/components/trade-clipboard/index.tsx | 454 +-- packages/web/components/types.ts | 19 +- packages/web/config/feature-flag.ts | 3 + packages/web/config/ibc-assets.ts | 5 +- packages/web/hooks/index.ts | 2 + packages/web/hooks/ui-config/index.ts | 5 + .../ui-config/use-add-liquidity-config.ts | 56 +- .../ui-config/use-bond-liquidity-config.ts | 37 + .../hooks/ui-config/use-lock-token-config.ts | 121 + .../hooks/ui-config/use-pool-detail-config.ts | 32 + .../web/hooks/ui-config/use-pool-gauges.ts | 102 + .../ui-config/use-remove-liquidity-config.ts | 34 +- .../ui-config/use-superfluid-pool-config.ts | 108 + packages/web/hooks/use-amplitude-analytics.ts | 34 +- .../use-connect-wallet-modal-redirect.tsx | 9 +- packages/web/hooks/use-nav-bar.ts | 22 + packages/web/hooks/user-settings/index.ts | 2 + .../user-settings/use-current-language.ts | 28 + .../user-settings/use-show-dust-filter.ts | 32 + .../axelar/source-chain-config.ts | 42 +- packages/web/integrations/axelar/transfer.tsx | 42 +- packages/web/integrations/axelar/types.ts | 4 +- packages/web/integrations/axelar/utils.ts | 5 +- packages/web/integrations/bridge-info.ts | 16 + .../integrations/ethereum/metamask-utils.ts | 3 - packages/web/integrations/kado/index.tsx | 23 + packages/web/integrations/transak/index.ts | 1 + .../integrations/transak/use-transak-modal.ts | 74 + packages/web/localizations/dayjs-locale-en.js | 22 + packages/web/localizations/dayjs-locale-es.js | 20 + packages/web/localizations/dayjs-locale-fr.js | 61 + packages/web/localizations/dayjs-locale-ko.js | 17 + packages/web/localizations/dayjs-locale-pl.js | 46 + packages/web/localizations/dayjs-locale-tr.js | 61 + packages/web/localizations/en.json | 376 ++ packages/web/localizations/es.json | 376 ++ packages/web/localizations/fr.json | 376 ++ packages/web/localizations/ko.json | 369 ++ packages/web/localizations/pl.json | 377 ++ packages/web/localizations/tr.json | 376 ++ packages/web/modals/add-liquidity.tsx | 65 + packages/web/modals/base.tsx | 6 +- packages/web/modals/bridge-transfer.tsx | 10 +- .../web/modals/connect-non-ibc-wallet.tsx | 93 - packages/web/modals/create-pool.tsx | 9 +- packages/web/modals/fiat-ramps.tsx | 43 + packages/web/modals/ibc-transfer.tsx | 31 +- packages/web/modals/{index.tsx => index.ts} | 10 +- .../web/modals/keplr-connection-selection.tsx | 152 +- packages/web/modals/lock-tokens.tsx | 356 +- packages/web/modals/manage-liquidity.tsx | 384 -- packages/web/modals/menu-options-icon.tsx | 43 + packages/web/modals/menu-options.tsx | 2 +- packages/web/modals/pre-transfer.tsx | 44 +- packages/web/modals/remove-liquidity.tsx | 67 + packages/web/modals/select-asset-source.tsx | 97 + packages/web/modals/superfluid-validator.tsx | 51 +- packages/web/modals/token-select.tsx | 22 +- packages/web/modals/trade-tokens.tsx | 4 +- packages/web/modals/transfer-asset-select.tsx | 31 +- packages/web/package.json | 3 + packages/web/pages/404.tsx | 21 +- packages/web/pages/500.tsx | 21 +- packages/web/pages/_app.tsx | 139 +- packages/web/pages/assets/index.tsx | 187 +- packages/web/pages/bootstrap/index.tsx | 6 +- packages/web/pages/disclaimer/index.tsx | 8 +- packages/web/pages/index.tsx | 19 +- packages/web/pages/pixels/index.tsx | 10 +- packages/web/pages/pool/[id].tsx | 1744 +++------ packages/web/pages/pools/index.tsx | 586 +-- packages/web/public/icons/add.svg | 4 +- .../public/icons/arrow-right-superfluid.svg | 4 + .../icons/arrow-right-wosmongton-300.svg | 4 + packages/web/public/icons/arrow-right.svg | 4 + packages/web/public/icons/asset-selected.svg | 10 - packages/web/public/icons/asset-white.svg | 9 +- packages/web/public/icons/asset.svg | 7 - packages/web/public/icons/chart-white.svg | 2 +- packages/web/public/icons/chart.svg | 7 - packages/web/public/icons/check-mark-dark.svg | 3 + .../web/public/icons/check-mark-surface.svg | 5 - .../web/public/icons/checkmark-circle.svg | 2 +- .../public/icons/chevron-down-secondary.svg | 4 - packages/web/public/icons/chevron-down.svg | 7 +- .../public/icons/chevron-left-secondary.svg | 5 - packages/web/public/icons/chevron-left.svg | 4 +- .../public/icons/chevron-right-disabled.svg | 3 + .../web/public/icons/chevron-right-rust.svg | 3 + packages/web/public/icons/chevron-right.svg | 8 +- .../public/icons/chevron-up-osmoverse-400.svg | 3 + ...hevron-up-secondary.svg => chevron-up.svg} | 2 +- packages/web/public/icons/commonwealth.svg | 11 - ...nk-secondary-200.svg => external-link.svg} | 2 +- packages/web/public/icons/hamburger.svg | 5 + .../web/public/icons/hexagon-border-card.svg | 4 - .../public/icons/hexagon-border-selected.svg | 4 - .../icons/hexagon-border-white-selected.svg | 4 - .../web/public/icons/hexagon-border-white.svg | 4 - packages/web/public/icons/hexagon-border.svg | 4 - .../web/public/icons/info-secondary-200.svg | 5 - packages/web/public/icons/left.svg | 3 + packages/web/public/icons/medium.svg | 5 - packages/web/public/icons/menu-white.svg | 5 - packages/web/public/icons/menu.svg | 5 - packages/web/public/icons/more-menu.svg | 5 + packages/web/public/icons/pool-selected.svg | 14 - packages/web/public/icons/pool-white.svg | 4 +- packages/web/public/icons/pool.svg | 10 - packages/web/public/icons/search-hollow.svg | 4 - packages/web/public/icons/search.svg | 9 +- .../web/public/icons/setting-selected.svg | 7 - packages/web/public/icons/setting-white.svg | 7 +- packages/web/public/icons/setting.svg | 7 +- .../web/public/icons/sign-out-secondary.svg | 3 - packages/web/public/icons/sort-down.svg | 2 +- packages/web/public/icons/sort-up.svg | 2 +- packages/web/public/icons/support-lab.svg | 5 + packages/web/public/icons/telegram.svg | 7 - packages/web/public/icons/ticket-white.svg | 2 +- packages/web/public/icons/ticket.svg | 7 - .../web/public/icons/trade-green-check.svg | 9 - packages/web/public/icons/trade-selected.svg | 14 - packages/web/public/icons/trade-white.svg | 4 +- packages/web/public/icons/trade.svg | 10 - packages/web/public/icons/twitter.svg | 5 - packages/web/public/icons/up-down-arrow.svg | 16 +- packages/web/public/icons/vote-white.svg | 2 +- packages/web/public/icons/vote.svg | 7 - packages/web/public/icons/warning.svg | 2 +- packages/web/public/images/keplr-logo.png | Bin 8128 -> 0 bytes packages/web/public/images/keplr-logo.svg | 10 + packages/web/public/images/lab-machine.svg | 797 +++++ packages/web/public/images/large-vial.svg | 510 +++ packages/web/public/images/medium-vial.svg | 278 ++ .../web/public/images/osmosis-guy-in-lab.png | Bin 268543 -> 0 bytes .../osmosis-home-bg-pattern-frontier.svg | 2 +- .../public/images/osmosis-home-bg-pattern.svg | 2 +- .../public/images/osmosis-liquidity-lab.png | Bin 95347 -> 0 bytes packages/web/public/images/preview.jpg | Bin 0 -> 176467 bytes packages/web/public/images/small-vial.svg | 164 + packages/web/public/logos/kado.svg | 1 + packages/web/public/logos/transak.svg | 1 + packages/web/stores/assets/index.ts | 1 + .../web/stores/assets/transfer-ui-config.ts | 79 +- packages/web/stores/assets/types.ts | 6 +- packages/web/stores/nav-bar.ts | 76 + packages/web/stores/root.ts | 39 +- packages/web/stores/user-settings/index.ts | 62 + .../web/stores/user-settings/language.tsx | 70 + .../web/stores/user-settings/show-dust.tsx | 41 + packages/web/styles/globals.css | 79 +- packages/web/tailwind.config.js | 166 +- yarn.lock | 3155 +++++++++-------- 300 files changed, 14260 insertions(+), 7928 deletions(-) delete mode 100644 CODEOWNERS create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 packages/stores/src/queries-external/base.ts create mode 100644 packages/stores/src/queries-external/pool-rewards/index.ts create mode 100644 packages/stores/src/queries-external/pool-rewards/types.ts create mode 100644 packages/stores/src/ui-config/errors.ts create mode 100644 packages/stores/src/ui-config/manage-liquidity/bond-liquidity.ts create mode 100644 packages/stores/src/ui-config/manage-liquidity/errors.ts create mode 100644 packages/stores/src/ui-config/user-config.ts create mode 100644 packages/stores/types/queries-external/base.d.ts create mode 100644 packages/stores/types/queries-external/pool-rewards/index.d.ts create mode 100644 packages/stores/types/queries-external/pool-rewards/types.d.ts create mode 100644 packages/stores/types/ui-config/errors.d.ts create mode 100644 packages/stores/types/ui-config/manage-liquidity/bond-liquidity.d.ts create mode 100644 packages/stores/types/ui-config/manage-liquidity/errors.d.ts create mode 100644 packages/stores/types/ui-config/user-config.d.ts delete mode 100644 packages/web/components/alert/error.tsx create mode 100644 packages/web/components/assets/fallback-img.tsx create mode 100644 packages/web/components/buttons/arrow-button.tsx create mode 100644 packages/web/components/buttons/border-button.tsx delete mode 100644 packages/web/components/buttons/icon-button.tsx rename packages/web/components/cards/{wallet.tsx => asset-source.tsx} (89%) delete mode 100644 packages/web/components/cards/base.tsx create mode 100644 packages/web/components/cards/bond-card.tsx delete mode 100644 packages/web/components/cards/go-superfluid.tsx delete mode 100644 packages/web/components/cards/incentivized-pool.tsx delete mode 100644 packages/web/components/cards/my-pool.tsx delete mode 100644 packages/web/components/cards/pool-catalyst.tsx delete mode 100644 packages/web/components/cards/pool-gauge-bonus.tsx delete mode 100644 packages/web/components/cards/pool-gauge.tsx delete mode 100644 packages/web/components/cards/superfluid-validator.tsx create mode 100644 packages/web/components/chart/asset-breakdown.tsx create mode 100644 packages/web/components/chart/price-breakdown.tsx create mode 100644 packages/web/components/complex/add-liquidity.tsx create mode 100644 packages/web/components/complex/remove-liquidity.tsx delete mode 100644 packages/web/components/complex/sidebar-bottom.tsx create mode 100644 packages/web/components/control/icon-dropdown/icon-dropdown.tsx create mode 100644 packages/web/components/control/icon-dropdown/index.ts create mode 100644 packages/web/components/control/icon-dropdown/menu-dropdown-icon-item.tsx create mode 100644 packages/web/components/control/icon-dropdown/menu-dropdown-icon.tsx create mode 100644 packages/web/components/control/language-select.tsx delete mode 100644 packages/web/components/left-time.tsx create mode 100644 packages/web/components/localization/formatted.tsx create mode 100644 packages/web/components/localization/index.ts create mode 100644 packages/web/components/localization/translate-error.ts create mode 100644 packages/web/components/main-menu.tsx create mode 100644 packages/web/components/navbar/index.tsx delete mode 100644 packages/web/components/overview/index.tsx delete mode 100644 packages/web/components/overview/overview-label-value.tsx create mode 100644 packages/web/components/overview/pools.tsx create mode 100644 packages/web/components/table/cells/pool-quick-actions.tsx create mode 100644 packages/web/hooks/ui-config/use-bond-liquidity-config.ts create mode 100644 packages/web/hooks/ui-config/use-lock-token-config.ts create mode 100644 packages/web/hooks/ui-config/use-pool-detail-config.ts create mode 100644 packages/web/hooks/ui-config/use-pool-gauges.ts create mode 100644 packages/web/hooks/ui-config/use-superfluid-pool-config.ts create mode 100644 packages/web/hooks/use-nav-bar.ts create mode 100644 packages/web/hooks/user-settings/index.ts create mode 100644 packages/web/hooks/user-settings/use-current-language.ts create mode 100644 packages/web/hooks/user-settings/use-show-dust-filter.ts create mode 100644 packages/web/integrations/kado/index.tsx create mode 100644 packages/web/integrations/transak/index.ts create mode 100644 packages/web/integrations/transak/use-transak-modal.ts create mode 100644 packages/web/localizations/dayjs-locale-en.js create mode 100644 packages/web/localizations/dayjs-locale-es.js create mode 100644 packages/web/localizations/dayjs-locale-fr.js create mode 100644 packages/web/localizations/dayjs-locale-ko.js create mode 100644 packages/web/localizations/dayjs-locale-pl.js create mode 100644 packages/web/localizations/dayjs-locale-tr.js create mode 100644 packages/web/localizations/en.json create mode 100644 packages/web/localizations/es.json create mode 100644 packages/web/localizations/fr.json create mode 100644 packages/web/localizations/ko.json create mode 100644 packages/web/localizations/pl.json create mode 100644 packages/web/localizations/tr.json create mode 100644 packages/web/modals/add-liquidity.tsx delete mode 100644 packages/web/modals/connect-non-ibc-wallet.tsx create mode 100644 packages/web/modals/fiat-ramps.tsx rename packages/web/modals/{index.tsx => index.ts} (66%) delete mode 100644 packages/web/modals/manage-liquidity.tsx create mode 100644 packages/web/modals/menu-options-icon.tsx create mode 100644 packages/web/modals/remove-liquidity.tsx create mode 100644 packages/web/modals/select-asset-source.tsx create mode 100644 packages/web/public/icons/arrow-right-superfluid.svg create mode 100644 packages/web/public/icons/arrow-right-wosmongton-300.svg create mode 100644 packages/web/public/icons/arrow-right.svg delete mode 100644 packages/web/public/icons/asset-selected.svg delete mode 100644 packages/web/public/icons/asset.svg delete mode 100644 packages/web/public/icons/chart.svg create mode 100644 packages/web/public/icons/check-mark-dark.svg delete mode 100644 packages/web/public/icons/check-mark-surface.svg delete mode 100644 packages/web/public/icons/chevron-down-secondary.svg delete mode 100644 packages/web/public/icons/chevron-left-secondary.svg create mode 100644 packages/web/public/icons/chevron-right-disabled.svg create mode 100644 packages/web/public/icons/chevron-right-rust.svg create mode 100644 packages/web/public/icons/chevron-up-osmoverse-400.svg rename packages/web/public/icons/{chevron-up-secondary.svg => chevron-up.svg} (95%) delete mode 100644 packages/web/public/icons/commonwealth.svg rename packages/web/public/icons/{external-link-secondary-200.svg => external-link.svg} (96%) create mode 100644 packages/web/public/icons/hamburger.svg delete mode 100644 packages/web/public/icons/hexagon-border-card.svg delete mode 100644 packages/web/public/icons/hexagon-border-selected.svg delete mode 100644 packages/web/public/icons/hexagon-border-white-selected.svg delete mode 100644 packages/web/public/icons/hexagon-border-white.svg delete mode 100644 packages/web/public/icons/hexagon-border.svg delete mode 100644 packages/web/public/icons/info-secondary-200.svg create mode 100644 packages/web/public/icons/left.svg delete mode 100644 packages/web/public/icons/medium.svg delete mode 100644 packages/web/public/icons/menu-white.svg delete mode 100644 packages/web/public/icons/menu.svg create mode 100644 packages/web/public/icons/more-menu.svg delete mode 100644 packages/web/public/icons/pool-selected.svg delete mode 100644 packages/web/public/icons/pool.svg delete mode 100644 packages/web/public/icons/search-hollow.svg delete mode 100644 packages/web/public/icons/setting-selected.svg delete mode 100644 packages/web/public/icons/sign-out-secondary.svg create mode 100644 packages/web/public/icons/support-lab.svg delete mode 100644 packages/web/public/icons/telegram.svg delete mode 100644 packages/web/public/icons/ticket.svg delete mode 100644 packages/web/public/icons/trade-green-check.svg delete mode 100644 packages/web/public/icons/trade-selected.svg delete mode 100644 packages/web/public/icons/trade.svg delete mode 100644 packages/web/public/icons/twitter.svg delete mode 100644 packages/web/public/icons/vote.svg delete mode 100644 packages/web/public/images/keplr-logo.png create mode 100644 packages/web/public/images/keplr-logo.svg create mode 100644 packages/web/public/images/lab-machine.svg create mode 100644 packages/web/public/images/large-vial.svg create mode 100644 packages/web/public/images/medium-vial.svg delete mode 100644 packages/web/public/images/osmosis-guy-in-lab.png delete mode 100644 packages/web/public/images/osmosis-liquidity-lab.png create mode 100644 packages/web/public/images/preview.jpg create mode 100644 packages/web/public/images/small-vial.svg create mode 100644 packages/web/public/logos/kado.svg create mode 100644 packages/web/public/logos/transak.svg create mode 100644 packages/web/stores/nav-bar.ts create mode 100644 packages/web/stores/user-settings/index.ts create mode 100644 packages/web/stores/user-settings/language.tsx create mode 100644 packages/web/stores/user-settings/show-dust.tsx diff --git a/.gitignore b/.gitignore index fbea566ca0..4bbed99656 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist prod build yarn-error.log +.yarn src/styles/index.scss .vercel diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 86dcf90412..0000000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -/packages/web/config/ @JeremyParish69 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..bab2188292 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:14.19.1 as build + +WORKDIR /usr/src/app + +COPY . . + +RUN npm install -g npm@8.10.0 + +RUN yarn +RUN yarn build + +FROM node:14.19.1-alpine + +WORKDIR /usr/src/app + +COPY --from=build /usr/src/app . + +EXPOSE 3000 + +CMD [ "yarn", "start" ] \ No newline at end of file diff --git a/README.md b/README.md index 2b549482a5..d0e17d2b06 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Otherwise the non-frontier commands can be used with the env var set to true. ### Testnet +Testnet version of the frontend uses `NEXT_PUBLIC_IS_TESTNET=true`. + Dev: ```bash diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..a5c2f59e83 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.5' + +services: + app: + build: + context: . + dockerfile: ./Dockerfile + ports: + - '3000:3000' \ No newline at end of file diff --git a/packages/stores/src/price/pool.ts b/packages/stores/src/price/pool.ts index 59840866a5..75130728d9 100644 --- a/packages/stores/src/price/pool.ts +++ b/packages/stores/src/price/pool.ts @@ -28,7 +28,9 @@ export class PoolFallbackPriceStore protected readonly queryPool: ObservableQueryPools, intermidiateRoutes: IntermediateRoute[] ) { - super(kvStore, supportedVsCurrencies, defaultVsCurrency); + super(kvStore, supportedVsCurrencies, defaultVsCurrency, { + baseURL: "https://prices.osmosis.zone/api/v3", + }); this._intermidiateRoutes = intermidiateRoutes; diff --git a/packages/stores/src/queries-external/base.ts b/packages/stores/src/queries-external/base.ts new file mode 100644 index 0000000000..0295b212be --- /dev/null +++ b/packages/stores/src/queries-external/base.ts @@ -0,0 +1,13 @@ +import Axios from "axios"; +import { ObservableQuery } from "@keplr-wallet/stores"; +import { KVStore } from "@keplr-wallet/common"; +export class ObservableQueryExternalBase< + T = unknown, + E = unknown +> extends ObservableQuery { + constructor(kvStore: KVStore, baseURL: string, urlPath: string) { + const instance = Axios.create({ baseURL }); + + super(kvStore, instance, urlPath); + } +} diff --git a/packages/stores/src/queries-external/index.ts b/packages/stores/src/queries-external/index.ts index 27a89272ab..2e999b936e 100644 --- a/packages/stores/src/queries-external/index.ts +++ b/packages/stores/src/queries-external/index.ts @@ -1,2 +1,8 @@ export * from "./pool-fees"; +export * from "./pool-rewards"; export * from "./store"; + +export const IMPERATOR_HISTORICAL_DATA_BASEURL = + "https://api-osmosis.imperator.co"; +export const IMPERATOR_TX_REWARD_BASEURL = + "https://api-osmosis-chain.imperator.co"; diff --git a/packages/stores/src/queries-external/pool-fees/index.ts b/packages/stores/src/queries-external/pool-fees/index.ts index ae5b0e63a8..93b3e11afb 100644 --- a/packages/stores/src/queries-external/pool-fees/index.ts +++ b/packages/stores/src/queries-external/pool-fees/index.ts @@ -2,40 +2,19 @@ import { makeObservable } from "mobx"; import { computedFn } from "mobx-utils"; import { KVStore } from "@keplr-wallet/common"; import { Dec, PricePretty, RatePretty } from "@keplr-wallet/unit"; -import { pow } from "@osmosis-labs/math"; import { IPriceStore } from "../../price"; import { ObservableQueryPool } from "../../queries/pools"; -import { ObservableQueryExternal } from "../store"; -import { - ObservablePoolWithFeeMetrics, - PoolFeesMetrics, - PoolFees, -} from "./types"; +import { ObservableQueryExternalBase } from "../base"; +import { PoolFeesMetrics, PoolFees } from "./types"; /** Queries Imperator pool fee history data. */ -export class ObservableQueryPoolFeesMetrics extends ObservableQueryExternal { +export class ObservableQueryPoolFeesMetrics extends ObservableQueryExternalBase { constructor(kvStore: KVStore, baseURL: string) { super(kvStore, baseURL, "/fees/v1/pools"); makeObservable(this); } - readonly makePoolWithFeeMetrics = computedFn( - ( - pool: ObservableQueryPool, - priceStore: IPriceStore - ): ObservablePoolWithFeeMetrics => { - const poolFeesMetrics = this.getPoolFeesMetrics(pool.id, priceStore); - const liquidity = pool.computeTotalValueLocked(priceStore); - - return { - pool, - liquidity, - ...poolFeesMetrics, - }; - } - ); - readonly getPoolFeesMetrics = computedFn( (poolId: string, priceStore: IPriceStore): PoolFeesMetrics => { const fiatCurrency = priceStore.getFiatCurrency( @@ -89,32 +68,20 @@ export class ObservableQueryPoolFeesMetrics extends ObservableQueryExternal { - const avgDayFeeRevenue = new Dec( - pool.feesSpent7d.toDec().toString(), - 6 - ).quo(new Dec(7)); - const poolTVL = pool.pool.computeTotalValueLocked(priceStore).toDec(); - - if (poolTVL.equals(new Dec(0))) { - return new RatePretty(0).ready(false); - } - const percentRevenue = avgDayFeeRevenue.quo(poolTVL); - const dailyRate = new Dec(1).add(percentRevenue); - - if (!dailyRate.lt(new Dec(2))) return new RatePretty(0); + /** Get pool non-incentivized return from fees based on past 7d of activity. */ + readonly get7dPoolFeeApr = computedFn( + (pool: ObservableQueryPool, priceStore: IPriceStore): RatePretty => { + const { feesSpent7d } = this.getPoolFeesMetrics(pool.id, priceStore); + const avgDayFeeRevenue = new Dec(feesSpent7d.toDec().toString(), 6).quo( + new Dec(7) + ); + const poolTVL = pool.computeTotalValueLocked(priceStore).toDec(); + const revenuePerYear = avgDayFeeRevenue.mul(new Dec(365)); - const rate = pow(dailyRate, new Dec(365)); + if (poolTVL.equals(new Dec(0)) || revenuePerYear.equals(new Dec(0))) + return new RatePretty(0); - return new RatePretty(rate) - .sub(new Dec(1)) - .mul(new Dec(2)) - .moveDecimalPointLeft(2); + return new RatePretty(revenuePerYear.quo(poolTVL)); } ); } diff --git a/packages/stores/src/queries-external/pool-fees/types.ts b/packages/stores/src/queries-external/pool-fees/types.ts index 73178dee7f..59593a9343 100644 --- a/packages/stores/src/queries-external/pool-fees/types.ts +++ b/packages/stores/src/queries-external/pool-fees/types.ts @@ -1,13 +1,4 @@ -import { PricePretty, RatePretty } from "@keplr-wallet/unit"; -import { ObservableQueryPool } from "../../queries/pools"; - -export interface ObservablePoolWithFeeMetrics extends PoolFeesMetrics { - pool: ObservableQueryPool; - liquidity: PricePretty; - myLiquidity?: PricePretty; - epochsRemaining?: number; - apr?: RatePretty; -} +import { PricePretty } from "@keplr-wallet/unit"; export interface PoolFeesMetrics { volume24h: PricePretty; diff --git a/packages/stores/src/queries-external/pool-rewards/index.ts b/packages/stores/src/queries-external/pool-rewards/index.ts new file mode 100644 index 0000000000..eeec7f30dd --- /dev/null +++ b/packages/stores/src/queries-external/pool-rewards/index.ts @@ -0,0 +1,70 @@ +import { makeObservable } from "mobx"; +import { computedFn } from "mobx-utils"; +import { KVStore } from "@keplr-wallet/common"; +import { HasMapStore } from "@keplr-wallet/stores"; +import { Dec, PricePretty } from "@keplr-wallet/unit"; +import { IPriceStore } from "../../price"; +import { ObservableQueryExternalBase } from "../base"; +import { PoolsRewards, PoolRewards } from "./types"; + +/** Queries Imperator pool fee history data. */ +export class ObservableQueryAccountPoolRewards extends ObservableQueryExternalBase { + constructor( + kvStore: KVStore, + baseURL: string, + protected readonly priceStore: IPriceStore, + protected readonly bech32Address: string + ) { + super(kvStore, baseURL, `/lp/v1/rewards/estimation/${bech32Address}`); + + makeObservable(this); + } + + protected canFetch(): boolean { + return this.bech32Address !== ""; + } + + readonly getUsdRewardsForPool = computedFn( + (poolId: string): PoolRewards | undefined => { + const fiat = this.priceStore.getFiatCurrency("usd"); + + if (!this.response || !fiat) return undefined; + + const poolRewards = this.response.data.pools[poolId] as + | PoolsRewards["pools"][0] + | undefined; + + if (!poolRewards) return undefined; + + return { + day: new PricePretty(fiat, new Dec(poolRewards.day_usd)), + month: new PricePretty(fiat, new Dec(poolRewards.month_usd)), + year: new PricePretty(fiat, new Dec(poolRewards.year_usd)), + }; + } + ); +} + +export class ObservableQueryAccountsPoolRewards extends HasMapStore { + constructor( + kvStore: KVStore, + priceStore: IPriceStore, + poolRewardsBaseUrl = "https://api-osmosis-chain.imperator.co" + ) { + super( + (bech32Address) => + new ObservableQueryAccountPoolRewards( + kvStore, + poolRewardsBaseUrl, + priceStore, + bech32Address + ) + ); + } + + get(bech32Address: string) { + return super.get(bech32Address) as ObservableQueryAccountPoolRewards; + } +} + +export * from "./types"; diff --git a/packages/stores/src/queries-external/pool-rewards/types.ts b/packages/stores/src/queries-external/pool-rewards/types.ts new file mode 100644 index 0000000000..12117955f3 --- /dev/null +++ b/packages/stores/src/queries-external/pool-rewards/types.ts @@ -0,0 +1,20 @@ +import { PricePretty } from "@keplr-wallet/unit"; + +export interface PoolsRewards { + pools: { + [id: string]: { + day_usd: number; + month_usd: number; + year_usd: number; + }; + }; + total_day_usd: number; + total_month_usd: number; + total_year_usd: number; +} + +export interface PoolRewards { + day: PricePretty; + month: PricePretty; + year: PricePretty; +} diff --git a/packages/stores/src/queries-external/store.ts b/packages/stores/src/queries-external/store.ts index 6615301277..7fa5fdd89c 100644 --- a/packages/stores/src/queries-external/store.ts +++ b/packages/stores/src/queries-external/store.ts @@ -1,41 +1,32 @@ import { KVStore } from "@keplr-wallet/common"; -import { HasMapStore, ObservableQuery } from "@keplr-wallet/stores"; import { DeepReadonly } from "utility-types"; +import { IPriceStore } from "../price"; import { ObservableQueryPoolFeesMetrics } from "./pool-fees"; -import Axios from "axios"; - -export class QueriesExternalStore extends HasMapStore { - constructor(protected readonly kvStore: KVStore, feeMetricsBaseURL?: string) { - super(() => new QueriesExternal(this.kvStore, feeMetricsBaseURL)); - } - - get(): QueriesExternal { - return super.get("external"); - } -} +import { ObservableQueryAccountsPoolRewards } from "./pool-rewards"; +import { + IMPERATOR_HISTORICAL_DATA_BASEURL, + IMPERATOR_TX_REWARD_BASEURL, +} from "."; /** Root store for queries external to any chain. */ -export class QueriesExternal { +export class QueriesExternalStore { public readonly queryGammPoolFeeMetrics: DeepReadonly; + public readonly queryAccountsPoolRewards: DeepReadonly; constructor( kvStore: KVStore, - feeMetricsBaseURL = "https://api-osmosis.imperator.co" + priceStore: IPriceStore, + feeMetricsBaseURL = IMPERATOR_HISTORICAL_DATA_BASEURL, + poolRewardsBaseUrl = IMPERATOR_TX_REWARD_BASEURL ) { this.queryGammPoolFeeMetrics = new ObservableQueryPoolFeesMetrics( kvStore, feeMetricsBaseURL ); - } -} - -export class ObservableQueryExternal< - T = unknown, - E = unknown -> extends ObservableQuery { - constructor(kvStore: KVStore, baseURL: string, urlPath: string) { - const instance = Axios.create({ baseURL }); - - super(kvStore, instance, urlPath); + this.queryAccountsPoolRewards = new ObservableQueryAccountsPoolRewards( + kvStore, + priceStore, + poolRewardsBaseUrl + ); } } diff --git a/packages/stores/src/queries/pool-incentives/incentivized-pools.ts b/packages/stores/src/queries/pool-incentives/incentivized-pools.ts index e8ea4a475c..56410b6ddf 100644 --- a/packages/stores/src/queries/pool-incentives/incentivized-pools.ts +++ b/packages/stores/src/queries/pool-incentives/incentivized-pools.ts @@ -1,7 +1,7 @@ import { KVStore } from "@keplr-wallet/common"; import { ChainGetter, ObservableChainQuery } from "@keplr-wallet/stores"; import { FiatCurrency } from "@keplr-wallet/types"; -import { Dec, Int, RatePretty } from "@keplr-wallet/unit"; +import { CoinPretty, Dec, Int, RatePretty } from "@keplr-wallet/unit"; import dayjs from "dayjs"; import { Duration } from "dayjs/plugin/duration"; import { computed, makeObservable } from "mobx"; @@ -88,9 +88,9 @@ export class ObservableQueryIncentivizedPools extends ObservableChainQuery { if (!this.isIncentivized(poolId)) { return new RatePretty(new Dec(0)); @@ -111,7 +111,7 @@ export class ObservableQueryIncentivizedPools extends ObservableChainQuery= duration.asMilliseconds()) { break; } - apy = apy.add( - this.computeAPYForSpecificDuration( + apr = apr.add( + this.computeAprForSpecificDuration( poolId, lockableDuration, priceStore, @@ -263,11 +261,107 @@ export class ObservableQueryIncentivizedPools extends ObservableChainQuery { + const gaugeId = this.getIncentivizedGaugeId(poolId, duration); + const incentiveBondDurations = + this.queryLockableDurations.lockableDurations; + + if (this.incentivizedPools.includes(poolId) && gaugeId) { + const pool = this.queryPools.getPool(poolId); + if (pool) { + const mintDenom = this.queryMintParmas.mintDenom; + const epochIdentifier = this.queryMintParmas.epochIdentifier; + + if (mintDenom && epochIdentifier) { + const epoch = this.queryEpochs.getEpoch(epochIdentifier); + + const chainInfo = this.chainGetter.getChain(this.chainId); + const mintCurrency = chainInfo.findCurrency(mintDenom); + + if (mintCurrency && mintCurrency.coinGeckoId && epoch.duration) { + const totalWeight = this.queryDistrInfo.totalWeight; + const potWeight = this.queryDistrInfo.getWeight(gaugeId); + const mintPrice = priceStore.getPrice( + mintCurrency.coinGeckoId, + fiatCurrency.currency + ); + const poolTVL = pool.computeTotalValueLocked(priceStore); + if ( + totalWeight.gt(new Int(0)) && + potWeight.gt(new Int(0)) && + mintPrice && + poolTVL.toDec().gt(new Dec(0)) + ) { + const epochProvision = this.queryEpochProvision.epochProvisions; + + if (epochProvision) { + const numEpochPerYear = + dayjs + .duration({ + years: 1, + }) + .asMilliseconds() / epoch.duration.asMilliseconds(); + + /** Issued over year. */ + const yearProvision = epochProvision.mul( + new Dec(numEpochPerYear.toString()) + ); + + const yearProvisionToPots = yearProvision.mul( + this.queryMintParmas.distributionProportions.poolIncentives + ); + + const curInternalBondDurationIndex = + incentiveBondDurations.reduce( + (defaultIndex, bondDuration, index) => { + if ( + bondDuration.asMilliseconds() === + duration.asMilliseconds() + ) { + return index; + } + return defaultIndex; + }, + 0 + ); + + const priorDuration = + incentiveBondDurations[curInternalBondDurationIndex - 1]; + + return yearProvisionToPots + .mul(new Dec(potWeight).quo(new Dec(totalWeight))) + .quo(new Dec(numEpochPerYear)) + .add( + // for internal incentives, higher bonding periods accrue incentives from prior gauges + priorDuration + ? this.computeDailyRewardForDuration( + poolId, + priorDuration, + priceStore, + fiatCurrency + ) ?? new Dec(0) + : new Dec(0) + ); + } + } + } + } + } + } + } + ); + + protected computeAprForSpecificDuration( poolId: string, duration: Duration, priceStore: IPriceStore, @@ -315,6 +409,7 @@ export class ObservableQueryIncentivizedPools extends ObservableChainQuery { const gaugeId = @@ -94,7 +89,7 @@ export class ObservableQueryPoolDetails { const gauge = this.queries.queryGauge.get(gaugeId); - const apr = this.queries.queryIncentivizedPools.computeAPY( + const apr = this.queries.queryIncentivizedPools.computeApr( this.queryPool.id, gauge.lockupDuration, this.priceStore, @@ -121,7 +116,7 @@ export class ObservableQueryPoolDetails { } @computed - get userLockedValue(): PricePretty { + get userShareValue(): PricePretty { return this.totalValueLocked.mul( this.queries.queryGammPoolShare.getAllGammShareRatio( this.bech32Address, @@ -183,7 +178,7 @@ export class ObservableQueryPoolDetails { this.queryPool.id ) ? new RatePretty( - this.queries.queryIncentivizedPools.computeAPY( + this.queries.queryIncentivizedPools.computeApr( this.queryPool.id, lockedAsset.duration, this.priceStore, @@ -248,6 +243,7 @@ export class ObservableQueryPoolDetails { return false; } + @computed get allExternalGauges(): ExternalGauge[] { const queryPoolGuageIds = this.queries.queryPoolsGaugeIds.get( this.queryPool.id @@ -284,6 +280,33 @@ export class ObservableQueryPoolDetails { ); } + @computed + get userStats(): + | { + totalShares: CoinPretty; + totalShareValue: PricePretty; + bondedValue: PricePretty; + unbondedValue: PricePretty; + currentDailyEarnings?: PricePretty; + } + | undefined { + const totalShares = this.queries.queryGammPoolShare.getAllGammShare( + this.bech32Address, + this.queryPool.id + ); + + if (totalShares.toDec().isZero()) return; + + // TODO: get $/day earned from imperator API + + return { + totalShares, + totalShareValue: this.userShareValue, + bondedValue: this.userBondedValue, + unbondedValue: this.userAvailableValue, + }; + } + readonly queryAllowedExternalGauges = computedFn( ( findCurrency: (denom: string) => AppCurrency | undefined, diff --git a/packages/stores/src/queries/superfluid-pools/pool.ts b/packages/stores/src/queries/superfluid-pools/pool.ts index c021661885..348d3b15a5 100644 --- a/packages/stores/src/queries/superfluid-pools/pool.ts +++ b/packages/stores/src/queries/superfluid-pools/pool.ts @@ -1,12 +1,13 @@ -import { computed, makeObservable, observable, action } from "mobx"; +import { computed, makeObservable } from "mobx"; import { FiatCurrency } from "@keplr-wallet/types"; import { ObservableQueryValidators, ObservableQueryInflation, Staking, } from "@keplr-wallet/stores"; -import { Dec, RatePretty } from "@keplr-wallet/unit"; +import { Dec, RatePretty, CoinPretty } from "@keplr-wallet/unit"; import { IPriceStore } from "../../price"; +import { UserConfig } from "../../ui-config"; import { ObservableQueryPoolDetails } from "../pools"; import { ObservableQueryGammPoolShare } from "../pool-share"; import { @@ -22,10 +23,7 @@ import { } from "../superfluid-pools"; /** Convenience store getting common superfluid data for a pool via superfluid stores. */ -export class ObservableQuerySuperfluidPool { - @observable - protected bech32Address: string = ""; - +export class ObservableQuerySuperfluidPool extends UserConfig { constructor( protected readonly fiatCurrency: FiatCurrency, protected readonly queryPoolDetails: ObservableQueryPoolDetails, @@ -43,14 +41,10 @@ export class ObservableQuerySuperfluidPool { }, protected readonly priceStore: IPriceStore ) { + super(); makeObservable(this); } - @action - setBech32Address(bech32Address: string) { - this.bech32Address = bech32Address; - } - @computed get isSuperfluid() { return this.queries.querySuperfluidPools.isSuperfluidPool( @@ -61,7 +55,7 @@ export class ObservableQuerySuperfluidPool { /** Wraps `gauges` member of pool detail store with potential superfluid APR info. */ @computed get gaugesWithSuperfluidApr() { - return this.queryPoolDetails.gauges.map((gaugeInfo) => { + return this.queryPoolDetails.internalGauges.map((gaugeInfo) => { const lastDuration = this.queryPoolDetails.longestDuration; return { ...gaugeInfo, @@ -100,34 +94,33 @@ export class ObservableQuerySuperfluidPool { } @computed - get upgradeableLpLockIds() { + get superfluid() { if (!this.isSuperfluid || !this.queryPoolDetails.longestDuration) return; + let upgradeableLpLockIds: + | { + amount: CoinPretty; + lockIds: string[]; + } + | undefined; if (this.queryPoolDetails.lockableDurations.length > 0) { - return this.queries.queryAccountLocked + upgradeableLpLockIds = this.queries.queryAccountLocked .get(this.bech32Address) .getLockedCoinWithDuration( this.queryPoolDetails.poolShareCurrency, this.queryPoolDetails.longestDuration ); } - } - - @computed - get superfluid() { - if (!this.isSuperfluid || !this.queryPoolDetails.longestDuration) return; const undelegatedLockedLpShares = (this.queries.querySuperfluidDelegations .getQuerySuperfluidDelegations(this.bech32Address) .getDelegations(this.queryPoolDetails.poolShareCurrency)?.length === 0 && - this.upgradeableLpLockIds && - this.upgradeableLpLockIds.lockIds.length > 0) ?? + upgradeableLpLockIds && + upgradeableLpLockIds.lockIds.length > 0) ?? false; - const upgradeableLpLockIds = this.upgradeableLpLockIds; - return undelegatedLockedLpShares ? { upgradeableLpLockIds } : { @@ -167,7 +160,7 @@ export class ObservableQuerySuperfluidPool { ); if (this.queryPoolDetails.lockableDurations.length > 0) { - const poolApr = this.queries.queryIncentivizedPools.computeAPY( + const poolApr = this.queries.queryIncentivizedPools.computeApr( this.queryPoolDetails.pool.id, this.queryPoolDetails.longestDuration, this.priceStore, diff --git a/packages/stores/src/ui-config/create-pool.ts b/packages/stores/src/ui-config/create-pool.ts index 9bba982f6e..ead3395041 100644 --- a/packages/stores/src/ui-config/create-pool.ts +++ b/packages/stores/src/ui-config/create-pool.ts @@ -1,5 +1,15 @@ -import { observable, computed, makeObservable, action } from "mobx"; -import { TxChainSetter, IFeeConfig } from "@keplr-wallet/hooks"; +import { + observable, + computed, + makeObservable, + action, + runInAction, +} from "mobx"; +import { + TxChainSetter, + IFeeConfig, + InvalidNumberAmountError, +} from "@keplr-wallet/hooks"; import { ObservableQueryBalances, ChainGetter, @@ -8,7 +18,16 @@ import { import { AmountConfig } from "@keplr-wallet/hooks"; import { AppCurrency } from "@keplr-wallet/types"; import { Dec, RatePretty } from "@keplr-wallet/unit"; -import { CREATE_POOL_MAX_ASSETS } from "."; +import { + DepositNoBalanceError, + HighSwapFeeError, + InvalidSwapFeeError, + MaxAssetsCountError, + MinAssetsCountError, + NegativePercentageError, + NegativeSwapFeeError, + PercentageSumError, +} from "./errors"; export interface CreatePoolConfigOpts { minAssetsCount: number; @@ -38,7 +57,7 @@ export class ObservableCreatePoolConfig extends TxChainSetter { protected _swapFee: string = "0"; @observable - public acknowledgeFee = false; + public _acknowledgeFee = false; protected _opts: CreatePoolConfigOpts; @@ -51,7 +70,7 @@ export class ObservableCreatePoolConfig extends TxChainSetter { feeConfig?: IFeeConfig, opts: CreatePoolConfigOpts = { minAssetsCount: 2, - maxAssetsCount: 8, + maxAssetsCount: 4, } ) { super(chainGetter, initialChainId); @@ -83,7 +102,7 @@ export class ObservableCreatePoolConfig extends TxChainSetter { get canAddAsset(): boolean { return ( - this._assets.length < CREATE_POOL_MAX_ASSETS && + this._assets.length < this._opts.maxAssetsCount && this.remainingSelectableCurrencies.length > 0 ); } @@ -101,6 +120,14 @@ export class ObservableCreatePoolConfig extends TxChainSetter { return this._queryBalances; } + get acknowledgeFee() { + return this._acknowledgeFee; + } + + set acknowledgeFee(ack: boolean) { + runInAction(() => (this._acknowledgeFee = ack)); + } + @computed get sendableCurrencies(): AppCurrency[] { return this._queryBalances @@ -144,18 +171,18 @@ export class ObservableCreatePoolConfig extends TxChainSetter { get positiveBalanceError(): Error | undefined { if (this.sendableCurrencies.length === 0) { - return new Error("You have no assets to deposit"); + return new DepositNoBalanceError("You have no assets to deposit"); } } get percentageError(): Error | undefined { if (this.assets.length < this._opts.minAssetsCount) { - return new Error( + return new MinAssetsCountError( `Minimum of ${this._opts.minAssetsCount} assets required` ); } if (this.assets.length > this._opts.maxAssetsCount) { - return new Error( + return new MaxAssetsCountError( `Maximumm of ${this._opts.maxAssetsCount} assets allowed` ); } @@ -166,16 +193,16 @@ export class ObservableCreatePoolConfig extends TxChainSetter { const percentage = new Dec(asset.percentage); if (percentage.lte(new Dec(0))) { - return new Error("Non-positive percentage"); + return new NegativePercentageError("Non-positive percentage"); } totalPercentage = totalPercentage.add(percentage); } catch { - return new Error("Invalid number"); + return new InvalidNumberAmountError("Invalid number"); } } if (!totalPercentage.equals(new Dec(100))) { - return new Error("Sum of percentages is not 100%"); + return new PercentageSumError("Sum of percentages is not 100"); } } @@ -183,13 +210,13 @@ export class ObservableCreatePoolConfig extends TxChainSetter { try { const dec = new Dec(this.swapFee); if (dec.lt(new Dec(0))) { - return new Error("Negative swap fee"); + return new NegativeSwapFeeError("Negative swap fee"); } if (dec.gte(new Dec(100))) { - return new Error("Swap fee too high"); + return new HighSwapFeeError("Swap fee too high"); } } catch { - return new Error("Invalid swap fee"); + return new InvalidSwapFeeError("Invalid swap fee"); } } diff --git a/packages/stores/src/ui-config/errors.ts b/packages/stores/src/ui-config/errors.ts new file mode 100644 index 0000000000..2fb7e62098 --- /dev/null +++ b/packages/stores/src/ui-config/errors.ts @@ -0,0 +1,97 @@ +export class NegativeSwapFeeError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, NegativeSwapFeeError.prototype); + } +} + +export class HighSwapFeeError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, HighSwapFeeError.prototype); + } +} + +export class InvalidSwapFeeError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, InvalidSwapFeeError.prototype); + } +} + +export class MinAssetsCountError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, MinAssetsCountError.prototype); + } +} + +export class MaxAssetsCountError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, MaxAssetsCountError.prototype); + } +} + +export class NegativePercentageError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, NegativePercentageError.prototype); + } +} + +export class PercentageSumError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, PercentageSumError.prototype); + } +} + +export class DepositNoBalanceError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, DepositNoBalanceError.prototype); + } +} + +export class NegativeSlippageError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, NegativeSlippageError.prototype); + } +} + +export class InvalidSlippageError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, InvalidSlippageError.prototype); + } +} + +export class NoSendCurrencyError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, NoSendCurrencyError.prototype); + } +} + +export class InsufficientBalanceError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, InsufficientBalanceError.prototype); + } +} + +export * from "./manage-liquidity/errors"; diff --git a/packages/stores/src/ui-config/index.ts b/packages/stores/src/ui-config/index.ts index c78d15a369..f39303d0db 100644 --- a/packages/stores/src/ui-config/index.ts +++ b/packages/stores/src/ui-config/index.ts @@ -1,7 +1,7 @@ export * from "./manage-liquidity"; export * from "./create-pool"; +export * from "./errors"; export * from "./fake-fee-config"; export * from "./slippage-config"; export * from "./trade-token-in-config"; - -export const CREATE_POOL_MAX_ASSETS = 8; +export * from "./user-config"; diff --git a/packages/stores/src/ui-config/manage-liquidity/add-liquidity.ts b/packages/stores/src/ui-config/manage-liquidity/add-liquidity.ts index 39937bb8e7..66f8f8debc 100644 --- a/packages/stores/src/ui-config/manage-liquidity/add-liquidity.ts +++ b/packages/stores/src/ui-config/manage-liquidity/add-liquidity.ts @@ -19,6 +19,7 @@ import { ObservableQueryGammPoolShare, } from "../../queries"; import { ManageLiquidityConfigBase } from "./base"; +import { NotInitializedError, CalculatingShareOutAmountError } from "./errors"; import { OSMO_MEDIUM_TX_FEE } from "."; /** Use to config user input UI for eventually sending a valid add liquidity msg. @@ -510,9 +511,9 @@ export class ObservableAddLiquidityConfig extends ManageLiquidityConfigBase { } @computed - get error() { + get error(): Error | undefined { if (this.poolAssetConfigs.length === 0) { - return new Error("Not initialized yet"); + return new NotInitializedError("Not initialized yet"); } if (this.isSingleAmountIn && this.singleAmountInConfig) { @@ -533,7 +534,9 @@ export class ObservableAddLiquidityConfig extends ManageLiquidityConfigBase { } if (!this.shareOutAmount || this.shareOutAmount.toDec().lte(new Dec(0))) { - return new Error("Calculating the share out amount"); + return new CalculatingShareOutAmountError( + "Calculating the share out amount" + ); } } } diff --git a/packages/stores/src/ui-config/manage-liquidity/bond-liquidity.ts b/packages/stores/src/ui-config/manage-liquidity/bond-liquidity.ts new file mode 100644 index 0000000000..742bfe1b0b --- /dev/null +++ b/packages/stores/src/ui-config/manage-liquidity/bond-liquidity.ts @@ -0,0 +1,255 @@ +import { makeObservable } from "mobx"; +import { computedFn } from "mobx-utils"; +import { Duration } from "dayjs/plugin/duration"; +import dayjs from "dayjs"; +import { CoinPretty, RatePretty, Dec, PricePretty } from "@keplr-wallet/unit"; +import { AppCurrency } from "@keplr-wallet/types"; +import { + ObservableQueryPoolDetails, + ObservableQuerySuperfluidPool, + ExternalGauge, + ObservableQueryAccountLocked, + ObservableQueryGuage, + ObservableQueryIncentivizedPools, +} from "../../queries"; +import { ObservableQueryPoolFeesMetrics } from "../../queries-external"; +import { IPriceStore } from "../../price"; +import { UserConfig } from "../user-config"; + +export type BondableDuration = { + duration: Duration; + userShares: CoinPretty; + userUnlockingShares?: { shares: CoinPretty; endTime?: Date }; + aggregateApr: RatePretty; + swapFeeApr: RatePretty; + swapFeeDailyReward: PricePretty; + incentivesBreakdown: { + dailyPoolReward: CoinPretty; + apr: RatePretty; + numDaysRemaining?: number; + }[]; + /** Both `delegated` and `undelegating` will be `undefined` if the user may "Go superfluid". */ + superfluid?: { + apr: RatePretty; + commission?: RatePretty; + validatorMoniker?: string; + validatorLogoUrl?: string; + delegated?: CoinPretty; + undelegating?: CoinPretty; + }; +}; + +export class ObservableBondLiquidityConfig extends UserConfig { + constructor( + protected readonly poolDetails: ObservableQueryPoolDetails, + protected readonly superfluidPool: ObservableQuerySuperfluidPool, + protected readonly priceStore: IPriceStore, + protected readonly queryFeeMetrics: ObservableQueryPoolFeesMetrics, + protected readonly queries: { + queryAccountLocked: ObservableQueryAccountLocked; + queryGauge: ObservableQueryGuage; + queryIncentivizedPools: ObservableQueryIncentivizedPools; + } + ) { + super(); + makeObservable(this); + } + + /** Calculates the stop in the bonding process the user is in. + * + * 1. Liquidity needs to be added + * 2. Liquidity needs to be bonded + */ + readonly calculateBondLevel = computedFn( + (bondableDurations: BondableDuration[]): 1 | 2 | undefined => { + if ( + this.poolDetails?.userAvailableValue.toDec().gt(new Dec(0)) && + bondableDurations.length > 0 + ) + return 2; + + if (this.poolDetails?.userAvailableValue.toDec().isZero()) return 1; + } + ); + + /** Gets all available durations for user to bond in, with a breakdown of the assets incentivizing the duration. Internal OSMO incentives & swap fees included in breakdown. */ + readonly getBondableAllowedDurations = computedFn( + ( + findCurrency: (denom: string) => AppCurrency | undefined, + allowedGauges: { gaugeId: string; denom: string }[] | undefined + ): BondableDuration[] => { + const poolId = this.poolDetails.pool.id; + const gauges = this.superfluidPool.gaugesWithSuperfluidApr; + + const externalGauges = allowedGauges + ? this.poolDetails.queryAllowedExternalGauges( + findCurrency, + allowedGauges + ) + : []; + + /** Set of all available durations. */ + const durationsMsSet = new Set(); + + (gauges as { duration: Duration }[]) + .concat(externalGauges as { duration: Duration }[]) + .forEach((gauge) => { + durationsMsSet.add(gauge.duration.asMilliseconds()); + }); + + return Array.from(durationsMsSet.values()).map((durationMs) => { + const curDuration = dayjs.duration({ + milliseconds: durationMs, + }); + + /** There is only one internal gauge of a chain-configured lockable duration (1,7,14 days). */ + const internalGaugeOfDuration = gauges.find( + (gauge) => gauge.duration.asMilliseconds() === durationMs + ); + const externalGaugesOfDuration = externalGauges.reduce( + (gauges, externalGauge) => { + if (externalGauge.duration.asMilliseconds() === durationMs) { + gauges.push(externalGauge); + } + return gauges; + }, + [] + ); + + const queryLockedCoin = this.queries.queryAccountLocked.get( + this.bech32Address + ); + const userShares = queryLockedCoin.getLockedCoinWithDuration( + this.poolDetails.poolShareCurrency, + curDuration + ).amount; + const allUnlockingCoins = queryLockedCoin.getUnlockingCoinWithDuration( + this.poolDetails.poolShareCurrency, + curDuration + ); + const userUnlockingShares = + allUnlockingCoins.length > 0 + ? { + // only return soonest unlocking shares + shares: + allUnlockingCoins[0]?.amount ?? + new CoinPretty(this.poolDetails.poolShareCurrency, 0), + endTime: allUnlockingCoins[0]?.endTime, + } + : undefined; + + const incentivesBreakdown: BondableDuration["incentivesBreakdown"] = []; + + // push single internal incentive for current duration + if (internalGaugeOfDuration) { + const { apr } = internalGaugeOfDuration; + + const fiatCurrency = this.priceStore.getFiatCurrency( + this.priceStore.defaultVsCurrency + ); + + if (fiatCurrency) { + const dailyPoolReward = + this.queries.queryIncentivizedPools.computeDailyRewardForDuration( + poolId, + curDuration, + this.priceStore, + fiatCurrency + ); + + if (dailyPoolReward) { + incentivesBreakdown.push({ + dailyPoolReward, + apr, + }); + } + } + } + + // push external incentives to current duration + externalGaugesOfDuration.forEach(({ id }) => { + const queryGauge = this.queries.queryGauge.get(id); + const allowedGauge = allowedGauges?.find( + ({ gaugeId }) => gaugeId === id + ); + if (!allowedGauge) return; + + const currency = findCurrency(allowedGauge.denom); + const fiatCurrency = this.priceStore.getFiatCurrency( + this.priceStore.defaultVsCurrency + ); + if (!currency || !fiatCurrency) return; + + incentivesBreakdown.push({ + dailyPoolReward: queryGauge + .getRemainingCoin(currency) + .quo(new Dec(queryGauge.remainingEpoch)), + apr: this.queries.queryIncentivizedPools.computeExternalIncentiveGaugeAPR( + poolId, + allowedGauge.gaugeId, + allowedGauge.denom, + this.priceStore, + fiatCurrency + ), + }); + }); + + // add superfluid data if highest duration + const sfsDuration = this.poolDetails.longestDuration; + let superfluid: BondableDuration["superfluid"] | undefined; + if ( + this.superfluidPool.isSuperfluid && + this.superfluidPool.superfluid && + sfsDuration && + curDuration.asSeconds() === sfsDuration.asSeconds() + ) { + const delegation = + (this.superfluidPool.superfluid.delegations?.length ?? 0) > 0 + ? this.superfluidPool.superfluid.delegations?.[0] + : undefined; + const undelegation = + (this.superfluidPool.superfluid.undelegations?.length ?? 0) > 0 + ? this.superfluidPool.superfluid.undelegations?.[0] + : undefined; + + superfluid = { + apr: this.superfluidPool.superfluidApr, + commission: delegation?.validatorCommission, + delegated: !this.superfluidPool.superfluid.upgradeableLpLockIds + ? delegation?.amount + : undefined, + undelegating: !this.superfluidPool.superfluid.upgradeableLpLockIds + ? undelegation?.amount + : undefined, + validatorMoniker: delegation?.validatorName, + validatorLogoUrl: delegation?.validatorImgSrc, + }; + } + + let aggregateApr = incentivesBreakdown.reduce( + (sum, { apr }) => sum.add(apr), + new RatePretty(0) + ); + const swapFeeApr = this.queryFeeMetrics.get7dPoolFeeApr( + this.poolDetails.pool, + this.priceStore + ); + aggregateApr = aggregateApr.add(swapFeeApr); + if (superfluid) aggregateApr = aggregateApr.add(superfluid.apr); + + return { + duration: curDuration, + userShares, + userUnlockingShares, + aggregateApr, + swapFeeApr, + swapFeeDailyReward: this.queryFeeMetrics + .getPoolFeesMetrics(poolId, this.priceStore) + .feesSpent7d.quo(new Dec(7)), + incentivesBreakdown, + superfluid, + }; + }); + } + ); +} diff --git a/packages/stores/src/ui-config/manage-liquidity/errors.ts b/packages/stores/src/ui-config/manage-liquidity/errors.ts new file mode 100644 index 0000000000..d3b2c86023 --- /dev/null +++ b/packages/stores/src/ui-config/manage-liquidity/errors.ts @@ -0,0 +1,23 @@ +export class NotInitializedError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, NotInitializedError.prototype); + } +} + +export class CalculatingShareOutAmountError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, CalculatingShareOutAmountError.prototype); + } +} + +export class NoAvailableSharesError extends Error { + constructor(m: string) { + super(m); + // Set the prototype explicitly. + Object.setPrototypeOf(this, NoAvailableSharesError.prototype); + } +} diff --git a/packages/stores/src/ui-config/manage-liquidity/index.ts b/packages/stores/src/ui-config/manage-liquidity/index.ts index e0352d3f1a..b04126c494 100644 --- a/packages/stores/src/ui-config/manage-liquidity/index.ts +++ b/packages/stores/src/ui-config/manage-liquidity/index.ts @@ -1,5 +1,6 @@ export * from "./add-liquidity"; export * from "./base"; +export * from "./bond-liquidity"; export * from "./remove-liquidity"; export const OSMO_MEDIUM_TX_FEE = "0.0125"; diff --git a/packages/stores/src/ui-config/manage-liquidity/remove-liquidity.ts b/packages/stores/src/ui-config/manage-liquidity/remove-liquidity.ts index 52eb3b30ca..bb2cb0c837 100644 --- a/packages/stores/src/ui-config/manage-liquidity/remove-liquidity.ts +++ b/packages/stores/src/ui-config/manage-liquidity/remove-liquidity.ts @@ -1,11 +1,14 @@ import { observable, makeObservable, action, computed } from "mobx"; -import { CoinPretty, Dec, DecUtils } from "@keplr-wallet/unit"; +import { computedFn } from "mobx-utils"; +import { CoinPretty, Dec, DecUtils, PricePretty } from "@keplr-wallet/unit"; import { ChainGetter, IQueriesStore } from "@keplr-wallet/stores"; +import { IPriceStore } from "../../price"; import { ObservableQueryGammPoolShare, ObservableQueryPools, } from "../../queries"; import { ManageLiquidityConfigBase } from "./base"; +import { NoAvailableSharesError } from "./errors"; /** Use to config user input UI for eventually sending a valid exit pool msg. * Included convenience functions for deriving pool asset amounts vs current input %. @@ -84,9 +87,27 @@ export class ObservableRemoveLiquidityConfig extends ManageLiquidityConfigBase { @computed get error(): Error | undefined { if (this.poolShare.toDec().isZero()) { - return new Error( + return new NoAvailableSharesError( `No available ${this.poolShare.currency.coinDenom} shares` ); } } + + /** Calculate value of currently selected pool shares. */ + readonly computePoolShareValueWithPercentage = computedFn( + (priceStore: IPriceStore) => { + const fiat = priceStore.getFiatCurrency(priceStore.defaultVsCurrency)!; + return this.poolShareAssetsWithPercentage.reduce( + (accummulatedValue, asset) => { + const assetPrice = priceStore.calculatePrice( + asset, + priceStore.defaultVsCurrency + ); + if (assetPrice) return accummulatedValue.add(assetPrice); + else return accummulatedValue; + }, + new PricePretty(fiat, 0) + ); + } + ); } diff --git a/packages/stores/src/ui-config/slippage-config.ts b/packages/stores/src/ui-config/slippage-config.ts index b7b5c8d163..eb04bff904 100644 --- a/packages/stores/src/ui-config/slippage-config.ts +++ b/packages/stores/src/ui-config/slippage-config.ts @@ -1,6 +1,6 @@ import { Dec, DecUtils, RatePretty } from "@keplr-wallet/unit"; import { action, computed, makeObservable, observable } from "mobx"; -import { computedFn } from "mobx-utils"; +import { InvalidSlippageError, NegativeSlippageError } from "./errors"; export class ObservableSlippageConfig { static readonly defaultSelectableSlippages: ReadonlyArray = [ @@ -121,20 +121,21 @@ export class ObservableSlippageConfig { }); } - getManualSlippageError = computedFn((): Error | undefined => { + @computed + get manualSlippageError(): Error | undefined { if (this._isManualSlippage) { try { const r = new RatePretty( new Dec(this._manualSlippage).quo(DecUtils.getTenExponentN(2)) ); if (r.toDec().isNegative()) { - return new Error("Slippage can not be negative"); + return new NegativeSlippageError("Slippage can not be negative"); } } catch { - return new Error("Invalid slippage"); + return new InvalidSlippageError("Invalid slippage"); } } return; - }); + } } diff --git a/packages/stores/src/ui-config/trade-token-in-config.ts b/packages/stores/src/ui-config/trade-token-in-config.ts index 3e6c9b6e75..1cf62f0ce6 100644 --- a/packages/stores/src/ui-config/trade-token-in-config.ts +++ b/packages/stores/src/ui-config/trade-token-in-config.ts @@ -1,9 +1,5 @@ import { action, computed, makeObservable, observable, override } from "mobx"; -import { - AmountConfig, - IFeeConfig, - InsufficientAmountError, -} from "@keplr-wallet/hooks"; +import { AmountConfig, IFeeConfig } from "@keplr-wallet/hooks"; import { ChainGetter, IQueriesStore } from "@keplr-wallet/stores"; import { AppCurrency } from "@keplr-wallet/types"; import { @@ -19,6 +15,7 @@ import { Pool, RoutePathWithAmount, } from "@osmosis-labs/pools"; +import { NoSendCurrencyError, InsufficientBalanceError } from "./errors"; export class ObservableTradeTokenInConfig extends AmountConfig { @observable.ref @@ -375,7 +372,7 @@ export class ObservableTradeTokenInConfig extends AmountConfig { get error(): Error | undefined { const sendCurrency = this.sendCurrency; if (!sendCurrency) { - return new Error("Currency to send not set"); + return new NoSendCurrencyError("Currency to send not set"); } if (this.amount) { @@ -386,7 +383,7 @@ export class ObservableTradeTokenInConfig extends AmountConfig { .getBalanceFromCurrency(this.sendCurrency); const balanceDec = balance.toDec(); if (dec.gt(balanceDec)) { - return new InsufficientAmountError("Insufficient balance"); + return new InsufficientBalanceError("Insufficient balance"); } } diff --git a/packages/stores/src/ui-config/user-config.ts b/packages/stores/src/ui-config/user-config.ts new file mode 100644 index 0000000000..f6d26c7c72 --- /dev/null +++ b/packages/stores/src/ui-config/user-config.ts @@ -0,0 +1,18 @@ +import { observable, makeObservable, action } from "mobx"; + +/** Simple base config dealing with a user's address. */ +export class UserConfig { + @observable + protected bech32Address: string = ""; + + constructor(bech32Address?: string) { + if (bech32Address) this.bech32Address = bech32Address; + + makeObservable(this); + } + + @action + setBech32Address(bech32Address: string) { + this.bech32Address = bech32Address; + } +} diff --git a/packages/stores/types/queries-external/base.d.ts b/packages/stores/types/queries-external/base.d.ts new file mode 100644 index 0000000000..b2aae99810 --- /dev/null +++ b/packages/stores/types/queries-external/base.d.ts @@ -0,0 +1,5 @@ +import { ObservableQuery } from "@keplr-wallet/stores"; +import { KVStore } from "@keplr-wallet/common"; +export declare class ObservableQueryExternalBase extends ObservableQuery { + constructor(kvStore: KVStore, baseURL: string, urlPath: string); +} diff --git a/packages/stores/types/queries-external/index.d.ts b/packages/stores/types/queries-external/index.d.ts index 27a89272ab..c57fcce445 100644 --- a/packages/stores/types/queries-external/index.d.ts +++ b/packages/stores/types/queries-external/index.d.ts @@ -1,2 +1,5 @@ export * from "./pool-fees"; +export * from "./pool-rewards"; export * from "./store"; +export declare const IMPERATOR_HISTORICAL_DATA_BASEURL = "https://api-osmosis.imperator.co"; +export declare const IMPERATOR_TX_REWARD_BASEURL = "https://api-osmosis-chain.imperator.co"; diff --git a/packages/stores/types/queries-external/pool-fees/index.d.ts b/packages/stores/types/queries-external/pool-fees/index.d.ts index 080c11ce2f..6e22e06646 100644 --- a/packages/stores/types/queries-external/pool-fees/index.d.ts +++ b/packages/stores/types/queries-external/pool-fees/index.d.ts @@ -2,14 +2,13 @@ import { KVStore } from "@keplr-wallet/common"; import { RatePretty } from "@keplr-wallet/unit"; import { IPriceStore } from "../../price"; import { ObservableQueryPool } from "../../queries/pools"; -import { ObservableQueryExternal } from "../store"; -import { ObservablePoolWithFeeMetrics, PoolFeesMetrics, PoolFees } from "./types"; +import { ObservableQueryExternalBase } from "../base"; +import { PoolFeesMetrics, PoolFees } from "./types"; /** Queries Imperator pool fee history data. */ -export declare class ObservableQueryPoolFeesMetrics extends ObservableQueryExternal { +export declare class ObservableQueryPoolFeesMetrics extends ObservableQueryExternalBase { constructor(kvStore: KVStore, baseURL: string); - readonly makePoolWithFeeMetrics: (pool: ObservableQueryPool, priceStore: IPriceStore) => ObservablePoolWithFeeMetrics; readonly getPoolFeesMetrics: (poolId: string, priceStore: IPriceStore) => PoolFeesMetrics; - /** Get pool non-incentivized return from fees based on past 7d of activity, compounded. */ - readonly get7dPoolFeeApy: (pool: ObservablePoolWithFeeMetrics, priceStore: IPriceStore) => RatePretty; + /** Get pool non-incentivized return from fees based on past 7d of activity. */ + readonly get7dPoolFeeApr: (pool: ObservableQueryPool, priceStore: IPriceStore) => RatePretty; } export * from "./types"; diff --git a/packages/stores/types/queries-external/pool-fees/types.d.ts b/packages/stores/types/queries-external/pool-fees/types.d.ts index d11125c072..2608d0b8d1 100644 --- a/packages/stores/types/queries-external/pool-fees/types.d.ts +++ b/packages/stores/types/queries-external/pool-fees/types.d.ts @@ -1,12 +1,4 @@ -import { PricePretty, RatePretty } from "@keplr-wallet/unit"; -import { ObservableQueryPool } from "../../queries/pools"; -export interface ObservablePoolWithFeeMetrics extends PoolFeesMetrics { - pool: ObservableQueryPool; - liquidity: PricePretty; - myLiquidity?: PricePretty; - epochsRemaining?: number; - apr?: RatePretty; -} +import { PricePretty } from "@keplr-wallet/unit"; export interface PoolFeesMetrics { volume24h: PricePretty; volume7d: PricePretty; diff --git a/packages/stores/types/queries-external/pool-rewards/index.d.ts b/packages/stores/types/queries-external/pool-rewards/index.d.ts new file mode 100644 index 0000000000..7eef520f80 --- /dev/null +++ b/packages/stores/types/queries-external/pool-rewards/index.d.ts @@ -0,0 +1,18 @@ +import { KVStore } from "@keplr-wallet/common"; +import { HasMapStore } from "@keplr-wallet/stores"; +import { IPriceStore } from "../../price"; +import { ObservableQueryExternalBase } from "../base"; +import { PoolsRewards, PoolRewards } from "./types"; +/** Queries Imperator pool fee history data. */ +export declare class ObservableQueryAccountPoolRewards extends ObservableQueryExternalBase { + protected readonly priceStore: IPriceStore; + protected readonly bech32Address: string; + constructor(kvStore: KVStore, baseURL: string, priceStore: IPriceStore, bech32Address: string); + protected canFetch(): boolean; + readonly getUsdRewardsForPool: (poolId: string) => PoolRewards | undefined; +} +export declare class ObservableQueryAccountsPoolRewards extends HasMapStore { + constructor(kvStore: KVStore, priceStore: IPriceStore, poolRewardsBaseUrl?: string); + get(bech32Address: string): ObservableQueryAccountPoolRewards; +} +export * from "./types"; diff --git a/packages/stores/types/queries-external/pool-rewards/types.d.ts b/packages/stores/types/queries-external/pool-rewards/types.d.ts new file mode 100644 index 0000000000..0a55dab119 --- /dev/null +++ b/packages/stores/types/queries-external/pool-rewards/types.d.ts @@ -0,0 +1,18 @@ +import { PricePretty } from "@keplr-wallet/unit"; +export interface PoolsRewards { + pools: { + [id: string]: { + day_usd: number; + month_usd: number; + year_usd: number; + }; + }; + total_day_usd: number; + total_month_usd: number; + total_year_usd: number; +} +export interface PoolRewards { + day: PricePretty; + month: PricePretty; + year: PricePretty; +} diff --git a/packages/stores/types/queries-external/store.d.ts b/packages/stores/types/queries-external/store.d.ts index ff91c7f71e..7b21990693 100644 --- a/packages/stores/types/queries-external/store.d.ts +++ b/packages/stores/types/queries-external/store.d.ts @@ -1,17 +1,11 @@ import { KVStore } from "@keplr-wallet/common"; -import { HasMapStore, ObservableQuery } from "@keplr-wallet/stores"; import { DeepReadonly } from "utility-types"; +import { IPriceStore } from "../price"; import { ObservableQueryPoolFeesMetrics } from "./pool-fees"; -export declare class QueriesExternalStore extends HasMapStore { - protected readonly kvStore: KVStore; - constructor(kvStore: KVStore, feeMetricsBaseURL?: string); - get(): QueriesExternal; -} +import { ObservableQueryAccountsPoolRewards } from "./pool-rewards"; /** Root store for queries external to any chain. */ -export declare class QueriesExternal { +export declare class QueriesExternalStore { readonly queryGammPoolFeeMetrics: DeepReadonly; - constructor(kvStore: KVStore, feeMetricsBaseURL?: string); -} -export declare class ObservableQueryExternal extends ObservableQuery { - constructor(kvStore: KVStore, baseURL: string, urlPath: string); + readonly queryAccountsPoolRewards: DeepReadonly; + constructor(kvStore: KVStore, priceStore: IPriceStore, feeMetricsBaseURL?: string, poolRewardsBaseUrl?: string); } diff --git a/packages/stores/types/queries/pool-incentives/incentivized-pools.d.ts b/packages/stores/types/queries/pool-incentives/incentivized-pools.d.ts index ae9b2edcd9..234e911b24 100644 --- a/packages/stores/types/queries/pool-incentives/incentivized-pools.d.ts +++ b/packages/stores/types/queries/pool-incentives/incentivized-pools.d.ts @@ -1,7 +1,7 @@ import { KVStore } from "@keplr-wallet/common"; import { ChainGetter, ObservableChainQuery } from "@keplr-wallet/stores"; import { FiatCurrency } from "@keplr-wallet/types"; -import { RatePretty } from "@keplr-wallet/unit"; +import { CoinPretty, RatePretty } from "@keplr-wallet/unit"; import { Duration } from "dayjs/plugin/duration"; import { ObservableQueryEpochs } from "../epochs"; import { ObservableQueryEpochProvisions, ObservableQueryMintParmas } from "../mint"; @@ -27,9 +27,9 @@ export declare class ObservableQueryIncentivizedPools extends ObservableChainQue /** Internal incentives (OSMO). */ readonly getIncentivizedGaugeId: (poolId: string, duration: Duration) => string | undefined; /** - * 가장 긴 lockable duration의 apy를 반환한다. + * Returns the APR of the longest lockable duration. */ - readonly computeMostAPY: (poolId: string, priceStore: IPriceStore) => RatePretty; + readonly computeMostApr: (poolId: string, priceStore: IPriceStore) => RatePretty; /** * Computes the external incentive APR for the given gaugeId and denom */ @@ -38,7 +38,8 @@ export declare class ObservableQueryIncentivizedPools extends ObservableChainQue * 리워드를 받을 수 있는 풀의 연당 이익률을 반환한다. * 리워드를 받을 수 없는 풀일 경우 0를 리턴한다. */ - readonly computeAPY: (poolId: string, duration: Duration, priceStore: IPriceStore, fiatCurrency: FiatCurrency) => RatePretty; - protected computeAPYForSpecificDuration(poolId: string, duration: Duration, priceStore: IPriceStore, fiatCurrency: FiatCurrency): RatePretty; + readonly computeApr: (poolId: string, duration: Duration, priceStore: IPriceStore, fiatCurrency: FiatCurrency) => RatePretty; + readonly computeDailyRewardForDuration: (poolId: string, duration: Duration, priceStore: IPriceStore, fiatCurrency: FiatCurrency) => CoinPretty | undefined; + protected computeAprForSpecificDuration(poolId: string, duration: Duration, priceStore: IPriceStore, fiatCurrency: FiatCurrency): RatePretty; get isAprFetching(): boolean; } diff --git a/packages/stores/types/queries/pools/pool-details.d.ts b/packages/stores/types/queries/pools/pool-details.d.ts index 853daa8b27..39d2e3d74e 100644 --- a/packages/stores/types/queries/pools/pool-details.d.ts +++ b/packages/stores/types/queries/pools/pool-details.d.ts @@ -2,6 +2,7 @@ import { Duration } from "dayjs/plugin/duration"; import { AppCurrency, FiatCurrency } from "@keplr-wallet/types"; import { PricePretty, RatePretty, CoinPretty } from "@keplr-wallet/unit"; import { IPriceStore } from "../../price"; +import { UserConfig } from "../../ui-config"; import { ObservableQueryGammPoolShare } from "../pool-share"; import { ObservableQueryIncentivizedPools, ObservableQueryLockableDurations, ObservableQueryPoolsGaugeIds } from "../pool-incentives"; import { ObservableQueryGuage } from "../incentives"; @@ -9,7 +10,7 @@ import { ObservableQueryAccountLocked, ObservableQueryAccountLockedCoins, Observ import { ObservableQueryPool } from "./pool"; import { ExternalGauge } from "./types"; /** Convenience store for getting common details of a pool via many other query stores. */ -export declare class ObservableQueryPoolDetails { +export declare class ObservableQueryPoolDetails extends UserConfig { protected readonly fiatCurrency: FiatCurrency; protected readonly queryPool: ObservableQueryPool; protected readonly queries: { @@ -23,7 +24,6 @@ export declare class ObservableQueryPoolDetails { queryPoolsGaugeIds: ObservableQueryPoolsGaugeIds; }; protected readonly priceStore: IPriceStore; - protected bech32Address: string; constructor(fiatCurrency: FiatCurrency, queryPool: ObservableQueryPool, queries: { queryGammPoolShare: ObservableQueryGammPoolShare; queryIncentivizedPools: ObservableQueryIncentivizedPools; @@ -34,20 +34,19 @@ export declare class ObservableQueryPoolDetails { queryLockableDurations: ObservableQueryLockableDurations; queryPoolsGaugeIds: ObservableQueryPoolsGaugeIds; }, priceStore: IPriceStore); - setBech32Address(bech32Address: string): void; get pool(): ObservableQueryPool; get poolShareCurrency(): import("@keplr-wallet/types").Currency; get isIncentivized(): boolean; get totalValueLocked(): PricePretty; get lockableDurations(): Duration[]; get longestDuration(): Duration; - get gauges(): { + get internalGauges(): { id: string; duration: Duration; apr: RatePretty; isLoading: boolean; }[]; - get userLockedValue(): PricePretty; + get userShareValue(): PricePretty; get userBondedValue(): PricePretty; get userAvailableValue(): PricePretty; get userPoolAssets(): { @@ -68,6 +67,13 @@ export declare class ObservableQueryPoolDetails { }[]; get userCanDepool(): boolean; get allExternalGauges(): ExternalGauge[]; + get userStats(): { + totalShares: CoinPretty; + totalShareValue: PricePretty; + bondedValue: PricePretty; + unbondedValue: PricePretty; + currentDailyEarnings?: PricePretty; + } | undefined; readonly queryAllowedExternalGauges: (findCurrency: (denom: string) => AppCurrency | undefined, allowedGauges: { gaugeId: string; denom: string; diff --git a/packages/stores/types/queries/superfluid-pools/pool.d.ts b/packages/stores/types/queries/superfluid-pools/pool.d.ts index 2375520bb1..f4c8928725 100644 --- a/packages/stores/types/queries/superfluid-pools/pool.d.ts +++ b/packages/stores/types/queries/superfluid-pools/pool.d.ts @@ -1,14 +1,15 @@ import { FiatCurrency } from "@keplr-wallet/types"; import { ObservableQueryValidators, ObservableQueryInflation } from "@keplr-wallet/stores"; -import { RatePretty } from "@keplr-wallet/unit"; +import { RatePretty, CoinPretty } from "@keplr-wallet/unit"; import { IPriceStore } from "../../price"; +import { UserConfig } from "../../ui-config"; import { ObservableQueryPoolDetails } from "../pools"; import { ObservableQueryGammPoolShare } from "../pool-share"; import { ObservableQueryLockableDurations, ObservableQueryIncentivizedPools } from "../pool-incentives"; import { ObservableQueryAccountLocked } from "../lockup"; import { ObservableQuerySuperfluidPools, ObservableQuerySuperfluidDelegations, ObservableQuerySuperfluidUndelegations, ObservableQuerySuperfluidOsmoEquivalent } from "../superfluid-pools"; /** Convenience store getting common superfluid data for a pool via superfluid stores. */ -export declare class ObservableQuerySuperfluidPool { +export declare class ObservableQuerySuperfluidPool extends UserConfig { protected readonly fiatCurrency: FiatCurrency; protected readonly queryPoolDetails: ObservableQueryPoolDetails; protected readonly queryValidators: ObservableQueryValidators; @@ -24,7 +25,6 @@ export declare class ObservableQuerySuperfluidPool { querySuperfluidOsmoEquivalent: ObservableQuerySuperfluidOsmoEquivalent; }; protected readonly priceStore: IPriceStore; - protected bech32Address: string; constructor(fiatCurrency: FiatCurrency, queryPoolDetails: ObservableQueryPoolDetails, queryValidators: ObservableQueryValidators, queryInflation: ObservableQueryInflation, queries: { queryGammPoolShare: ObservableQueryGammPoolShare; queryLockableDurations: ObservableQueryLockableDurations; @@ -35,7 +35,6 @@ export declare class ObservableQuerySuperfluidPool { querySuperfluidUndelegations: ObservableQuerySuperfluidUndelegations; querySuperfluidOsmoEquivalent: ObservableQuerySuperfluidOsmoEquivalent; }, priceStore: IPriceStore); - setBech32Address(bech32Address: string): void; get isSuperfluid(): boolean; /** Wraps `gauges` member of pool detail store with potential superfluid APR info. */ get gaugesWithSuperfluidApr(): { @@ -46,13 +45,9 @@ export declare class ObservableQuerySuperfluidPool { isLoading: boolean; }[]; get superfluidApr(): RatePretty; - get upgradeableLpLockIds(): { - amount: import("@keplr-wallet/unit").CoinPretty; - lockIds: string[]; - } | undefined; get superfluid(): { upgradeableLpLockIds: { - amount: import("@keplr-wallet/unit").CoinPretty; + amount: CoinPretty; lockIds: string[]; } | undefined; delegations?: undefined; @@ -65,16 +60,16 @@ export declare class ObservableQuerySuperfluidPool { validatorImgSrc: string | undefined; inactive: string | undefined; apr: RatePretty; - amount: import("@keplr-wallet/unit").CoinPretty; + amount: CoinPretty; }[] | undefined; undelegations: { validatorName: string | undefined; inactive: string | undefined; - amount: import("@keplr-wallet/unit").CoinPretty; + amount: CoinPretty; endTime: Date; }[] | undefined; superfluidLpShares: { - amount: import("@keplr-wallet/unit").CoinPretty; + amount: CoinPretty; lockIds: string[]; }; upgradeableLpLockIds?: undefined; diff --git a/packages/stores/types/ui-config/create-pool.d.ts b/packages/stores/types/ui-config/create-pool.d.ts index 5f474de0f3..f9dc998561 100644 --- a/packages/stores/types/ui-config/create-pool.d.ts +++ b/packages/stores/types/ui-config/create-pool.d.ts @@ -17,7 +17,7 @@ export declare class ObservableCreatePoolConfig extends TxChainSetter { amountConfig: AmountConfig; }[]; protected _swapFee: string; - acknowledgeFee: boolean; + _acknowledgeFee: boolean; protected _opts: CreatePoolConfigOpts; constructor(chainGetter: ChainGetter, initialChainId: string, sender: string, queriesStore: IQueriesStore, queryBalances: ObservableQueryBalances, feeConfig?: IFeeConfig, opts?: CreatePoolConfigOpts); get feeConfig(): IFeeConfig | undefined; @@ -30,6 +30,8 @@ export declare class ObservableCreatePoolConfig extends TxChainSetter { get sender(): string; setSender(bech32Address: string): void; get queryBalances(): ObservableQueryBalances; + get acknowledgeFee(): boolean; + set acknowledgeFee(ack: boolean); get sendableCurrencies(): AppCurrency[]; get swapFee(): string; /** diff --git a/packages/stores/types/ui-config/errors.d.ts b/packages/stores/types/ui-config/errors.d.ts new file mode 100644 index 0000000000..34b7cb9ac2 --- /dev/null +++ b/packages/stores/types/ui-config/errors.d.ts @@ -0,0 +1,37 @@ +export declare class NegativeSwapFeeError extends Error { + constructor(m: string); +} +export declare class HighSwapFeeError extends Error { + constructor(m: string); +} +export declare class InvalidSwapFeeError extends Error { + constructor(m: string); +} +export declare class MinAssetsCountError extends Error { + constructor(m: string); +} +export declare class MaxAssetsCountError extends Error { + constructor(m: string); +} +export declare class NegativePercentageError extends Error { + constructor(m: string); +} +export declare class PercentageSumError extends Error { + constructor(m: string); +} +export declare class DepositNoBalanceError extends Error { + constructor(m: string); +} +export declare class NegativeSlippageError extends Error { + constructor(m: string); +} +export declare class InvalidSlippageError extends Error { + constructor(m: string); +} +export declare class NoSendCurrencyError extends Error { + constructor(m: string); +} +export declare class InsufficientBalanceError extends Error { + constructor(m: string); +} +export * from "./manage-liquidity/errors"; diff --git a/packages/stores/types/ui-config/index.d.ts b/packages/stores/types/ui-config/index.d.ts index 8c01b3e3ea..f39303d0db 100644 --- a/packages/stores/types/ui-config/index.d.ts +++ b/packages/stores/types/ui-config/index.d.ts @@ -1,6 +1,7 @@ export * from "./manage-liquidity"; export * from "./create-pool"; +export * from "./errors"; export * from "./fake-fee-config"; export * from "./slippage-config"; export * from "./trade-token-in-config"; -export declare const CREATE_POOL_MAX_ASSETS = 8; +export * from "./user-config"; diff --git a/packages/stores/types/ui-config/manage-liquidity/bond-liquidity.d.ts b/packages/stores/types/ui-config/manage-liquidity/bond-liquidity.d.ts new file mode 100644 index 0000000000..9a6240ee0e --- /dev/null +++ b/packages/stores/types/ui-config/manage-liquidity/bond-liquidity.d.ts @@ -0,0 +1,59 @@ +import { Duration } from "dayjs/plugin/duration"; +import { CoinPretty, RatePretty, PricePretty } from "@keplr-wallet/unit"; +import { AppCurrency } from "@keplr-wallet/types"; +import { ObservableQueryPoolDetails, ObservableQuerySuperfluidPool, ObservableQueryAccountLocked, ObservableQueryGuage, ObservableQueryIncentivizedPools } from "../../queries"; +import { ObservableQueryPoolFeesMetrics } from "../../queries-external"; +import { IPriceStore } from "../../price"; +import { UserConfig } from "../user-config"; +export declare type BondableDuration = { + duration: Duration; + userShares: CoinPretty; + userUnlockingShares?: { + shares: CoinPretty; + endTime?: Date; + }; + aggregateApr: RatePretty; + swapFeeApr: RatePretty; + swapFeeDailyReward: PricePretty; + incentivesBreakdown: { + dailyPoolReward: CoinPretty; + apr: RatePretty; + numDaysRemaining?: number; + }[]; + /** Both `delegated` and `undelegating` will be `undefined` if the user may "Go superfluid". */ + superfluid?: { + apr: RatePretty; + commission?: RatePretty; + validatorMoniker?: string; + validatorLogoUrl?: string; + delegated?: CoinPretty; + undelegating?: CoinPretty; + }; +}; +export declare class ObservableBondLiquidityConfig extends UserConfig { + protected readonly poolDetails: ObservableQueryPoolDetails; + protected readonly superfluidPool: ObservableQuerySuperfluidPool; + protected readonly priceStore: IPriceStore; + protected readonly queryFeeMetrics: ObservableQueryPoolFeesMetrics; + protected readonly queries: { + queryAccountLocked: ObservableQueryAccountLocked; + queryGauge: ObservableQueryGuage; + queryIncentivizedPools: ObservableQueryIncentivizedPools; + }; + constructor(poolDetails: ObservableQueryPoolDetails, superfluidPool: ObservableQuerySuperfluidPool, priceStore: IPriceStore, queryFeeMetrics: ObservableQueryPoolFeesMetrics, queries: { + queryAccountLocked: ObservableQueryAccountLocked; + queryGauge: ObservableQueryGuage; + queryIncentivizedPools: ObservableQueryIncentivizedPools; + }); + /** Calculates the stop in the bonding process the user is in. + * + * 1. Liquidity needs to be added + * 2. Liquidity needs to be bonded + */ + readonly calculateBondLevel: (bondableDurations: BondableDuration[]) => 1 | 2 | undefined; + /** Gets all available durations for user to bond in, with a breakdown of the assets incentivizing the duration. Internal OSMO incentives & swap fees included in breakdown. */ + readonly getBondableAllowedDurations: (findCurrency: (denom: string) => AppCurrency | undefined, allowedGauges: { + gaugeId: string; + denom: string; + }[] | undefined) => BondableDuration[]; +} diff --git a/packages/stores/types/ui-config/manage-liquidity/errors.d.ts b/packages/stores/types/ui-config/manage-liquidity/errors.d.ts new file mode 100644 index 0000000000..0be106475d --- /dev/null +++ b/packages/stores/types/ui-config/manage-liquidity/errors.d.ts @@ -0,0 +1,9 @@ +export declare class NotInitializedError extends Error { + constructor(m: string); +} +export declare class CalculatingShareOutAmountError extends Error { + constructor(m: string); +} +export declare class NoAvailableSharesError extends Error { + constructor(m: string); +} diff --git a/packages/stores/types/ui-config/manage-liquidity/index.d.ts b/packages/stores/types/ui-config/manage-liquidity/index.d.ts index 222ddbbd81..e5d23bc2db 100644 --- a/packages/stores/types/ui-config/manage-liquidity/index.d.ts +++ b/packages/stores/types/ui-config/manage-liquidity/index.d.ts @@ -1,4 +1,5 @@ export * from "./add-liquidity"; export * from "./base"; +export * from "./bond-liquidity"; export * from "./remove-liquidity"; export declare const OSMO_MEDIUM_TX_FEE = "0.0125"; diff --git a/packages/stores/types/ui-config/manage-liquidity/remove-liquidity.d.ts b/packages/stores/types/ui-config/manage-liquidity/remove-liquidity.d.ts index ff236037b5..83da58dfae 100644 --- a/packages/stores/types/ui-config/manage-liquidity/remove-liquidity.d.ts +++ b/packages/stores/types/ui-config/manage-liquidity/remove-liquidity.d.ts @@ -1,5 +1,6 @@ -import { CoinPretty } from "@keplr-wallet/unit"; +import { CoinPretty, PricePretty } from "@keplr-wallet/unit"; import { ChainGetter, IQueriesStore } from "@keplr-wallet/stores"; +import { IPriceStore } from "../../price"; import { ObservableQueryGammPoolShare, ObservableQueryPools } from "../../queries"; import { ManageLiquidityConfigBase } from "./base"; /** Use to config user input UI for eventually sending a valid exit pool msg. @@ -17,4 +18,6 @@ export declare class ObservableRemoveLiquidityConfig extends ManageLiquidityConf /** Pool asset amounts equivalent to senders's unbonded gamm share vs percentage. */ get poolShareAssetsWithPercentage(): CoinPretty[]; get error(): Error | undefined; + /** Calculate value of currently selected pool shares. */ + readonly computePoolShareValueWithPercentage: (priceStore: IPriceStore) => PricePretty; } diff --git a/packages/stores/types/ui-config/slippage-config.d.ts b/packages/stores/types/ui-config/slippage-config.d.ts index 99ed57b83f..f65cf6769d 100644 --- a/packages/stores/types/ui-config/slippage-config.d.ts +++ b/packages/stores/types/ui-config/slippage-config.d.ts @@ -19,5 +19,5 @@ export declare class ObservableSlippageConfig { index: number; selected: boolean; }[]; - getManualSlippageError: () => Error | undefined; + get manualSlippageError(): Error | undefined; } diff --git a/packages/stores/types/ui-config/user-config.d.ts b/packages/stores/types/ui-config/user-config.d.ts new file mode 100644 index 0000000000..74cf40789b --- /dev/null +++ b/packages/stores/types/ui-config/user-config.d.ts @@ -0,0 +1,6 @@ +/** Simple base config dealing with a user's address. */ +export declare class UserConfig { + protected bech32Address: string; + constructor(bech32Address?: string); + setBech32Address(bech32Address: string): void; +} diff --git a/packages/web/components/alert/error.tsx b/packages/web/components/alert/error.tsx deleted file mode 100644 index dd2ad4d33b..0000000000 --- a/packages/web/components/alert/error.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import Image from "next/image"; -import { FunctionComponent } from "react"; -import classNames from "classnames"; -import { CustomClasses } from "../types"; -import { Alert } from "./types"; - -export const Error: FunctionComponent< - Pick & { iconSize?: "sm" | "md" } & CustomClasses -> = ({ message, iconSize = "sm", className }) => ( -
- error - {message} -
-); diff --git a/packages/web/components/alert/index.ts b/packages/web/components/alert/index.ts index 752edd887f..b840b43396 100644 --- a/packages/web/components/alert/index.ts +++ b/packages/web/components/alert/index.ts @@ -1,4 +1,3 @@ -export * from "./error"; export * from "./info"; export * from "./temp-banner"; export * from "./toast"; diff --git a/packages/web/components/alert/info.tsx b/packages/web/components/alert/info.tsx index 599ed9b11e..e3519d31a7 100644 --- a/packages/web/components/alert/info.tsx +++ b/packages/web/components/alert/info.tsx @@ -1,17 +1,27 @@ -import Image from "next/image"; import { FunctionComponent } from "react"; import classNames from "classnames"; import { CustomClasses, MobileProps } from "../types"; import { Alert } from "./types"; export const Info: FunctionComponent< - { size?: "large" | "subtle" } & Alert & { data?: string } & CustomClasses & + { size?: "large" | "subtle" } & Alert & { + data?: string; + borderClassName?: string; + } & CustomClasses & MobileProps -> = ({ size = "large", message, caption, data, className, isMobile = false }) => +> = ({ + size = "large", + message, + caption, + data, + borderClassName, + className, + isMobile = false, +}) => size === "subtle" ? (
@@ -20,22 +30,18 @@ export const Info: FunctionComponent< ) : (
-
- error -
{isMobile ? ( @@ -47,7 +53,9 @@ export const Info: FunctionComponent<
{message}
)} {caption && ( - {caption} + + {caption} + )}
{!isMobile && data && ( diff --git a/packages/web/components/alert/temp-banner.tsx b/packages/web/components/alert/temp-banner.tsx index 5636351a6c..5f60fe63ac 100644 --- a/packages/web/components/alert/temp-banner.tsx +++ b/packages/web/components/alert/temp-banner.tsx @@ -32,8 +32,7 @@ export const TempBanner: FunctionComponent<{ className={classNames( "fixed flex place-content-between right-3 top-3 text-white-high md:w-[330px] w-[596px] rounded-2xl", { - "border border-enabledGold": !IS_FRONTIER, - "bg-background": !IS_FRONTIER, + "bg-osmoverse-900": !IS_FRONTIER, }, IS_FRONTIER ? "py-3" : "py-2" )} @@ -56,11 +55,7 @@ export const TempBanner: FunctionComponent<{
info diff --git a/packages/web/components/alert/toast.tsx b/packages/web/components/alert/toast.tsx index 0b92b1ef93..c94267f5e7 100644 --- a/packages/web/components/alert/toast.tsx +++ b/packages/web/components/alert/toast.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import { FunctionComponent } from "react"; +import { useTranslation } from "react-multi-lang"; import { toast, ToastOptions } from "react-toastify"; import { Alert, ToastType } from "./types"; @@ -48,63 +49,72 @@ export function displayToast( } } -const LoadingToast: FunctionComponent = ({ message, caption }) => ( -
-
- loading +const LoadingToast: FunctionComponent = ({ message, caption }) => { + const t = useTranslation(); + return ( +
+
+ loading +
+
+
{t(message)}
+ {caption &&

{t(caption)}

} +
-
-
{message}
- {caption &&

{caption}

} -
-
-); + ); +}; -const ErrorToast: FunctionComponent = ({ message, caption }) => ( -
-
- failed -
-
-
{message}
- {caption &&

{caption}

} +const ErrorToast: FunctionComponent = ({ message, caption }) => { + const t = useTranslation(); + return ( +
+
+ failed +
+
+
{t(message)}
+ {caption &&

{t(caption)}

} +
-
-); + ); +}; const SuccessToast: FunctionComponent = ({ message, learnMoreUrl, learnMoreUrlCaption, -}) => ( -
-
- b -
-
-
{message}
- {learnMoreUrl && ( - - {learnMoreUrlCaption ?? "Learn more"} -
- link -
-
- )} +}) => { + const t = useTranslation(); + return ( +
+
+ b +
+
+
{t(message)}
+ {learnMoreUrl && ( + + {learnMoreUrlCaption ?? t("Learn more")} +
+ link +
+
+ )} +
-
-); + ); +}; diff --git a/packages/web/components/animation/bridge/index.tsx b/packages/web/components/animation/bridge/index.tsx index a59328f0ce..b645aae48c 100644 --- a/packages/web/components/animation/bridge/index.tsx +++ b/packages/web/components/animation/bridge/index.tsx @@ -6,6 +6,7 @@ import { truncateString } from "../../utils"; import { useWindowSize } from "../../../hooks"; import { CustomClasses, LoadingProps } from "../../types"; import { Animation as AnimationProps } from "../types"; +import { useTranslation } from "react-multi-lang"; const Lottie = dynamic(() => import("react-lottie"), { ssr: false }); @@ -28,6 +29,7 @@ export const BridgeAnimation: FunctionComponent< className, } = props; const { isMobile } = useWindowSize(); + const t = useTranslation(); const longFromName = from.networkName.length > 7; const longToName = to.networkName.length > 7; @@ -89,7 +91,7 @@ export const BridgeAnimation: FunctionComponent< { "opacity-30": bridge?.isLoading } )} > - From{" "} + {t("assets.transfer.from")}{" "} {truncateString(from.networkName, bridge ? (isMobile ? 10 : 12) : 18)}
@@ -111,7 +113,10 @@ export const BridgeAnimation: FunctionComponent< } )} > - {bridge?.isLoading ? "Loading" : "via"} {bridge.bridgeName} + {bridge?.isLoading + ? t("assets.transfer.loading") + : t("assets.transfer.via")}{" "} + {bridge.bridgeName}
)} @@ -135,7 +140,7 @@ export const BridgeAnimation: FunctionComponent< { "opacity-30": bridge?.isLoading } )} > - To{" "} + {t("assets.transfer.to")}{" "} {truncateString(to.networkName, bridge ? (isMobile ? 10 : 12) : 18)}
diff --git a/packages/web/components/assets/catalyst-icon.tsx b/packages/web/components/assets/catalyst-icon.tsx index 90d4813ebf..01a27bec46 100644 --- a/packages/web/components/assets/catalyst-icon.tsx +++ b/packages/web/components/assets/catalyst-icon.tsx @@ -19,7 +19,6 @@ export const CatalystIcon: FunctionComponent< "h-20 w-20": size === "md" && !isMobile, "h-16 w-16": size === "sm" && !isMobile, "h-14 w-14": isMobile, - "border border-enabledGold": !isLoading, }, className )} diff --git a/packages/web/components/assets/fallback-img.tsx b/packages/web/components/assets/fallback-img.tsx new file mode 100644 index 0000000000..68f46f3dab --- /dev/null +++ b/packages/web/components/assets/fallback-img.tsx @@ -0,0 +1,23 @@ +import { FunctionComponent, ImgHTMLAttributes, useRef } from "react"; + +export const FallbackImg: FunctionComponent< + { + /** Image to show if there is an error rendering/fetching default image src. */ + fallbacksrc: string; + } & ImgHTMLAttributes +> = (props) => { + const imgRef = useRef(null); + + return ( + {props.alt { + if (imgRef.current) { + imgRef.current.src = props.fallbacksrc; + } + }} + /> + ); +}; diff --git a/packages/web/components/assets/gradient-view.tsx b/packages/web/components/assets/gradient-view.tsx index ec975ff1ce..58dc935030 100644 --- a/packages/web/components/assets/gradient-view.tsx +++ b/packages/web/components/assets/gradient-view.tsx @@ -7,7 +7,7 @@ export const GradientView: FunctionComponent< { gradientClassName?: string; bgClassName?: string } & CustomClasses > = ({ gradientClassName = "bg-superfluid", - bgClassName = "bg-background", + bgClassName = "bg-osmoverse-900", className, children, }) => ( diff --git a/packages/web/components/assets/index.tsx b/packages/web/components/assets/index.tsx index acef7979cd..6b5e030965 100644 --- a/packages/web/components/assets/index.tsx +++ b/packages/web/components/assets/index.tsx @@ -1,3 +1,4 @@ +export * from "./fallback-img"; export * from "./pool-assets-icon"; export * from "./pool-assets-name"; export * from "./rate-ring"; diff --git a/packages/web/components/assets/pool-assets-icon.tsx b/packages/web/components/assets/pool-assets-icon.tsx index f872c866c3..38349ceb4d 100644 --- a/packages/web/components/assets/pool-assets-icon.tsx +++ b/packages/web/components/assets/pool-assets-icon.tsx @@ -8,8 +8,6 @@ interface Props { size?: "sm" | "md"; } -// TODO: handle one asset - export const PoolAssetsIcon: FunctionComponent = ({ assets, size = "md", @@ -20,35 +18,35 @@ export const PoolAssetsIcon: FunctionComponent = ({
{assets[0].coinImageUrl ? ( {assets[0].coinDenom} ) : ( no token icon )}
{assets.length >= 3 ? ( @@ -59,15 +57,15 @@ export const PoolAssetsIcon: FunctionComponent = ({ {assets[1].coinDenom} ) : ( no token icon )}
diff --git a/packages/web/components/assets/pool-assets-name.tsx b/packages/web/components/assets/pool-assets-name.tsx index de5d3b3830..ccf00c2e81 100644 --- a/packages/web/components/assets/pool-assets-name.tsx +++ b/packages/web/components/assets/pool-assets-name.tsx @@ -22,8 +22,10 @@ export const PoolAssetsName: FunctionComponent<{ ) .join(size === "sm" ? "/" : " / "); return size === "sm" ? ( - {assetsName} + + {assetsName} + ) : ( -
{assetsName}
+
{assetsName}
); }; diff --git a/packages/web/components/assets/token.tsx b/packages/web/components/assets/token.tsx index 4b3ba6fa1a..83eff9d5d5 100644 --- a/packages/web/components/assets/token.tsx +++ b/packages/web/components/assets/token.tsx @@ -21,21 +21,26 @@ export const Token: FunctionComponent< return (
- {poolShare && ( + {poolShare && !isMobile && ( )} -
+
{isMobile ? (
{truncateString(justCoinDenom)}
) : (
{truncateString(justCoinDenom)}
)} {networkName && !isMobile && ( - {networkName} + {networkName} + )} + {poolShare && isMobile && ( + + {poolShare.toString()} + )}
diff --git a/packages/web/components/buttons/arrow-button.tsx b/packages/web/components/buttons/arrow-button.tsx new file mode 100644 index 0000000000..c322a0dfcb --- /dev/null +++ b/packages/web/components/buttons/arrow-button.tsx @@ -0,0 +1,23 @@ +import Image from "next/image"; +import { FunctionComponent, ButtonHTMLAttributes } from "react"; +import classNames from "classnames"; + +export const ArrowButton: FunctionComponent< + ButtonHTMLAttributes +> = (props) => ( + +); diff --git a/packages/web/components/buttons/border-button.tsx b/packages/web/components/buttons/border-button.tsx new file mode 100644 index 0000000000..d2b93d8b75 --- /dev/null +++ b/packages/web/components/buttons/border-button.tsx @@ -0,0 +1,19 @@ +import { FunctionComponent } from "react"; +import classNames from "classnames"; +import { CustomClasses, Disableable } from "../types"; +import { ButtonProps } from "./types"; + +export const BorderButton: FunctionComponent< + ButtonProps & CustomClasses & Disableable +> = ({ onClick, className, disabled, children }) => ( + +); diff --git a/packages/web/components/buttons/button.tsx b/packages/web/components/buttons/button.tsx index ef037c568b..b1f8dfe67d 100644 --- a/packages/web/components/buttons/button.tsx +++ b/packages/web/components/buttons/button.tsx @@ -1,80 +1,59 @@ -import Image from "next/image"; -import { FunctionComponent } from "react"; import classNames from "classnames"; -import { CustomClasses, Disableable } from "../types"; -import { ButtonProps } from "./types"; +import { ButtonHTMLAttributes, FunctionComponent } from "react"; +import { CustomClasses } from "../types"; +import { IS_FRONTIER } from "../../config"; -interface Props extends ButtonProps, CustomClasses, Disableable { - color?: "primary" | "secondary" | "error"; - size?: "xs" | "sm" | "lg"; - type?: "block" | "arrow" | "arrow-sm" | "outline"; - loading?: boolean; -} +export const Button: FunctionComponent< + { + mode?: "primary" | "primary-warning" | "secondary" | "tertiary"; + size?: "sm" | "normal"; + } & CustomClasses & + ButtonHTMLAttributes +> = (props) => { + const { mode = "primary", size = "normal", className, children } = props; -export const Button: FunctionComponent = ({ - onClick, - color = "primary", - size = "md", - type = "block", - loading = false, - className, - disabled = false, - children, -}) => ( - -); + {typeof children === "string" ? ( + size === "sm" ? ( + + {children} + + ) : ( +
+ {children} +
+ ) + ) : ( + children + )} + + ); +}; diff --git a/packages/web/components/buttons/close-button.tsx b/packages/web/components/buttons/close-button.tsx index 90446d5f74..1ed030120a 100644 --- a/packages/web/components/buttons/close-button.tsx +++ b/packages/web/components/buttons/close-button.tsx @@ -9,7 +9,7 @@ export const CloseButton: FunctionComponent< > = ({ onClick, className, disabled }) => (
= ({ - onClick, - color = "primary", - size = "sm", - type = "chevron-right", - disabled = false, -}) => ( - -); diff --git a/packages/web/components/buttons/index.ts b/packages/web/components/buttons/index.ts index b694f0e016..fe1d5572de 100644 --- a/packages/web/components/buttons/index.ts +++ b/packages/web/components/buttons/index.ts @@ -1,3 +1,4 @@ +export * from "./arrow-button"; +export * from "./border-button"; export * from "./button"; export * from "./close-button"; -export * from "./icon-button"; diff --git a/packages/web/components/buttons/show-more.tsx b/packages/web/components/buttons/show-more.tsx index d55be10c9b..9a6557eaef 100644 --- a/packages/web/components/buttons/show-more.tsx +++ b/packages/web/components/buttons/show-more.tsx @@ -3,30 +3,30 @@ import { FunctionComponent } from "react"; import classNames from "classnames"; import { ToggleProps } from "../control"; import { CustomClasses } from "../types"; +import { useTranslation } from "react-multi-lang"; export const ShowMoreButton: FunctionComponent = ({ isOn, onToggle, className, -}) => ( - -); +}) => { + const t = useTranslation(); + return ( + + ); +}; diff --git a/packages/web/components/buttons/switch-wallet.tsx b/packages/web/components/buttons/switch-wallet.tsx index d0581c3873..9f3545fc68 100644 --- a/packages/web/components/buttons/switch-wallet.tsx +++ b/packages/web/components/buttons/switch-wallet.tsx @@ -7,7 +7,7 @@ export const SwitchWalletButton: FunctionComponent< ButtonProps & Disableable & { selectedWalletIconUrl: string } > = ({ onClick, disabled, selectedWalletIconUrl }) => ( + )} +
+
setDrawerUp(false)} + /> + setDrawerUp(!drawerUp)} + onGoSuperfluid={onGoSuperfluid} + /> +
+ ); +}; + +const Drawer: FunctionComponent<{ + aggregateApr: RatePretty; + swapFeeApr: RatePretty; + swapFeeDailyReward: PricePretty; + userShares: CoinPretty; + incentivesBreakdown: BondableDuration["incentivesBreakdown"]; + superfluid: BondableDuration["superfluid"]; + drawerUp: boolean; + toggleDetailsVisible: () => void; + onGoSuperfluid: () => void; +}> = ({ + aggregateApr, + swapFeeApr, + swapFeeDailyReward, + incentivesBreakdown, + superfluid, + drawerUp, + toggleDetailsVisible, +}) => { + const uniqueCoinImages = useMemo(() => { + const imgSrcDenomMap = new Map(); + incentivesBreakdown.forEach((breakdown) => { + const currency = breakdown.dailyPoolReward.currency; + if (currency.coinImageUrl) { + imgSrcDenomMap.set(currency.coinDenom, currency.coinImageUrl); + } + }); + return Array.from(imgSrcDenomMap.values()); + }, [incentivesBreakdown]); + const t = useTranslation(); + + return ( +
+
+
+ + {t("pool.incentives")} + +
+
+ {aggregateApr.maxDecimals(0).toString()} {t("pool.APR")} +
+
+ {uniqueCoinImages.map((coinImageUrl, index) => ( +
+ {index === 2 && incentivesBreakdown.length > 3 ? ( + + +{incentivesBreakdown.length - 2} + + ) : index < 2 ? ( + incentive icon + ) : null} +
+ ))} +
+
+
+ +
+
+
+ {incentivesBreakdown.map((breakdown, index) => ( +
+ {index === 0 && superfluid && ( + + )} + + {index === incentivesBreakdown.length - 1 && ( + + )} +
+ ))} +
+ + {t("pool.rewardDistribution")} + +
+
+ ); +}; + +const SuperfluidBreakdownRow: FunctionComponent< + BondableDuration["superfluid"] +> = ({ + apr, + commission, + delegated, + undelegating, + validatorMoniker, + validatorLogoUrl, +}) => { + const t = useTranslation(); + return ( +
+
+
+
+ +{apr.maxDecimals(0).toString()} +
+ +
+ + {(delegated || undelegating) && validatorMoniker + ? validatorMoniker + : t("pool.superfluidStaking")} + +
+ {(delegated || undelegating) && ( +
+
+ + {delegated + ? `~${delegated.trim(true).maxDecimals(7).toString()}` + : undelegating + ? `~${undelegating.trim(true).maxDecimals(7).toString()}` + : null} + + {delegated + ? ` ${t("pool.delegated")}` + : undelegating + ? ` ${t("pool.undelegating")}` + : ""} + + + {commission && ( + + {commission.toString()}{" "} + + {t("pool.commission")} + + + )} +
+
+ )} +
+ ); +}; + +const IncentiveBreakdownRow: FunctionComponent< + BondableDuration["incentivesBreakdown"][0] +> = ({ dailyPoolReward, apr, numDaysRemaining }) => { + const t = useTranslation(); + return ( +
+
+
+{apr.maxDecimals(0).toString()}
+ {dailyPoolReward.currency.coinImageUrl && ( + token icon + )} +
+
+ + {t("pool.dailyEarnAmount", { + amount: dailyPoolReward.maxDecimals(0).toString(), + })} + + {numDaysRemaining && ( + {numDaysRemaining} + )} +
+
+ ); +}; + +const SwapFeeBreakdownRow: FunctionComponent<{ + swapFeeApr: RatePretty; + swapFeeDailyReward: PricePretty; +}> = ({ swapFeeApr, swapFeeDailyReward }) => { + const t = useTranslation(); + return ( +
+
+
+ +{swapFeeApr.maxDecimals(0).toString()} +
+
+
+ + {t("pool.dailyEarnAmount", { + amount: swapFeeDailyReward.maxDecimals(0).toString(), + })} + + + {`${t("pool.from")} `} + + {t("pool.swapFees")} + {" "} + {t("pool.7davg")} + +
+
+ ); +}; diff --git a/packages/web/components/cards/go-superfluid.tsx b/packages/web/components/cards/go-superfluid.tsx deleted file mode 100644 index 176980f19d..0000000000 --- a/packages/web/components/cards/go-superfluid.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FunctionComponent } from "react"; -import { MobileProps } from "../types"; - -export const GoSuperfluidCard: FunctionComponent< - { - goSuperfluid: () => void; - } & MobileProps -> = ({ goSuperfluid, isMobile = false }) => ( -
-
-
- {isMobile - ? "You're not Superfluid Staking" - : "Superfluid Staking Inactive"} -
-
- You have superfluid eligible bonded liquidity. -
- Choose a Superfluid Staking validator to earn additional rewards. -
-
- -
-); diff --git a/packages/web/components/cards/incentivized-pool.tsx b/packages/web/components/cards/incentivized-pool.tsx deleted file mode 100644 index 2ce738eab2..0000000000 --- a/packages/web/components/cards/incentivized-pool.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { ObservableQueryPool } from "@osmosis-labs/stores"; -import { observer } from "mobx-react-lite"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import React, { FunctionComponent } from "react"; -import { useDeterministicIntegerFromString } from "../../hooks"; -import { useStore } from "../../stores"; -import { PoolCardBase, PoolCardIconBackgroundColors } from "./base"; -import { StatLabelValue } from "./stat-label-value"; - -export const IncentivizedPoolCard: FunctionComponent<{ - pool: ObservableQueryPool; -}> = observer(({ pool }) => { - const { chainStore, queriesStore, priceStore } = useStore(); - - const chainInfo = chainStore.osmosis; - const queryOsmosis = queriesStore.get(chainInfo.chainId).osmosis!; - - const router = useRouter(); - - const deterministicInteger = useDeterministicIntegerFromString(pool.id); - - const poolTVL = pool.computeTotalValueLocked(priceStore); - - const apr = queryOsmosis.queryIncentivizedPools.computeMostAPY( - pool.id, - priceStore - ); - - return ( - asset.amount.currency.coinDenom) - .join("/")} - icon={OSMO} - iconBackgroundColor={ - PoolCardIconBackgroundColors[ - deterministicInteger % PoolCardIconBackgroundColors.length - ] - } - onClick={() => { - router.push(`/pool/${pool.id}`); - }} - > -
- -
- -
- - ); -}); diff --git a/packages/web/components/cards/index.tsx b/packages/web/components/cards/index.tsx index 4d02a9c242..a5bcf138a2 100644 --- a/packages/web/components/cards/index.tsx +++ b/packages/web/components/cards/index.tsx @@ -1,12 +1,5 @@ export * from "./asset-card"; -export * from "./base"; -export * from "./go-superfluid"; -export * from "./incentivized-pool"; +export * from "./asset-source"; +export * from "./bond-card"; export * from "./stat-label-value"; -export * from "./my-pool"; -export * from "./wallet"; export * from "./pool-card"; -export * from "./pool-catalyst"; -export * from "./pool-gauge-bonus"; -export * from "./pool-gauge"; -export * from "./superfluid-validator"; diff --git a/packages/web/components/cards/my-pool.tsx b/packages/web/components/cards/my-pool.tsx deleted file mode 100644 index 20567ee381..0000000000 --- a/packages/web/components/cards/my-pool.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { FunctionComponent } from "react"; -import { ObservableQueryPool } from "@osmosis-labs/stores"; -import { observer } from "mobx-react-lite"; -import { useStore } from "../../stores"; -import { PoolCardBase, PoolCardIconBackgroundColors } from "./base"; -import Image from "next/image"; -import { useDeterministicIntegerFromString } from "../../hooks"; -import { StatLabelValue } from "./stat-label-value"; -import { useRouter } from "next/router"; - -export const MyPoolCard: FunctionComponent<{ - pool: ObservableQueryPool; -}> = observer(({ pool }) => { - const { chainStore, queriesStore, priceStore, accountStore } = useStore(); - - const chainInfo = chainStore.osmosis; - const queryOsmosis = queriesStore.get(chainInfo.chainId).osmosis!; - const account = accountStore.getAccount(chainInfo.chainId); - - const router = useRouter(); - - const deterministicInteger = useDeterministicIntegerFromString(pool.id); - - const poolTVL = pool.computeTotalValueLocked(priceStore); - - const apr = queryOsmosis.queryIncentivizedPools.computeMostAPY( - pool.id, - priceStore - ); - - const shareRatio = queryOsmosis.queryGammPoolShare.getAllGammShareRatio( - account.bech32Address, - pool.id - ); - const lockedShareRatio = - queryOsmosis.queryGammPoolShare.getLockedGammShareRatio( - account.bech32Address, - pool.id - ); - const myLiquidity = poolTVL.mul(shareRatio); - const myLockedAmount = poolTVL.mul(lockedShareRatio); - - return ( - asset.amount.currency.coinDenom) - .join("/")} - icon={OSMO} - iconBackgroundColor={ - PoolCardIconBackgroundColors[ - deterministicInteger % PoolCardIconBackgroundColors.length - ] - } - onClick={() => { - router.push(`/pool/${pool.id}`); - }} - > -
-
- - -
-
-
- - -
-
- - ); -}); diff --git a/packages/web/components/cards/pool-card.tsx b/packages/web/components/cards/pool-card.tsx index 5951efc7b8..6f5c79abbb 100644 --- a/packages/web/components/cards/pool-card.tsx +++ b/packages/web/components/cards/pool-card.tsx @@ -6,7 +6,10 @@ import { PoolAssetsIcon, PoolAssetsName } from "../assets"; import { PoolAssetInfo } from "../assets/types"; import { Metric } from "../types"; import { CustomClasses } from "../types"; -import { useWindowSize } from "../../hooks"; +import { useTranslation } from "react-multi-lang"; + +// notes: turn off prefetch to avoid loading tons of pools and lagging the client, many pools will be in viewport. They will still be fetched on hover. +// See : https://nextjs.org/docs/api-reference/next/link export const PoolCard: FunctionComponent< { @@ -17,114 +20,52 @@ export const PoolCard: FunctionComponent< mobileShowFirstLabel?: boolean; onClick?: () => void; } & CustomClasses -> = observer( - ({ - poolId, - poolAssets, - poolMetrics, - isSuperfluid, - mobileShowFirstLabel = false, - onClick, - className, - }) => { - const { isMobile } = useWindowSize(); - - // notes: turn off prefetch to avoid loading tons of pools and lagging the client, many pools will be in viewport. They will still be fetched on hover. - // See : https://nextjs.org/docs/api-reference/next/link +> = observer(({ poolId, poolAssets, poolMetrics, isSuperfluid, onClick }) => { + const t = useTranslation(); - if (isMobile) { - return ( - - -
-
- - -
- asset.coinDenom)} - /> - - Pool #{poolId} - -
-
-
- {poolMetrics.map((metric, index) => ( - - {metric.value}{" "} - {(mobileShowFirstLabel || index !== 0) && metric.label} - - ))} + return ( + + { + onClick?.(); + }} + > +
+
+ +
+ asset.coinDenom)} + /> +
+ {t("pools.poolId", { id: poolId })}
-
- - ); - } - - return ( - - { - onClick?.(); - }} - > - + + + ); +}); diff --git a/packages/web/components/cards/pool-catalyst.tsx b/packages/web/components/cards/pool-catalyst.tsx deleted file mode 100644 index 60d34dfdc7..0000000000 --- a/packages/web/components/cards/pool-catalyst.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { FunctionComponent } from "react"; -import classNames from "classnames"; -import { CustomClasses, MobileProps } from "../types"; -import { MetricLoader } from "../loaders"; -import { CatalystIcon } from "../assets/catalyst-icon"; -import { Metric, LoadingProps } from "../types"; -import { truncateString } from "../utils"; - -export const PoolCatalystCard: FunctionComponent< - { - colorKey?: number; - percentDec?: string; - tokenDenom?: string; - metrics: Metric[]; - } & CustomClasses & - LoadingProps & - MobileProps -> = ({ - colorKey, - percentDec, - tokenDenom, - metrics, - className, - isLoading, - isMobile, -}) => ( -
-
- -
- - {isMobile ?
{percentDec ?? ""}
:

{percentDec ?? ""}

} -
- - - {truncateString(tokenDenom ?? "", 28)} - - -
-
-
- {metrics.map(({ label, value }, index) => { - return ( -
- - {label} - - - {typeof value === "string" ? ( - isMobile ? ( - {value} - ) : ( -
{value}
- ) - ) : ( - <>{value} - )} -
-
- ); - })} -
-
-); diff --git a/packages/web/components/cards/pool-gauge-bonus.tsx b/packages/web/components/cards/pool-gauge-bonus.tsx deleted file mode 100644 index 1f8c428e10..0000000000 --- a/packages/web/components/cards/pool-gauge-bonus.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FunctionComponent } from "react"; -import { MetricLoader } from "../loaders"; -import { LoadingProps, MobileProps } from "../types"; - -export const PoolGaugeBonusCard: FunctionComponent< - { - bonusValue?: string; - days?: string; - remainingEpochs?: string; - } & LoadingProps & - MobileProps -> = ({ bonusValue, days, remainingEpochs, isLoading, isMobile = false }) => ( -
- {isMobile ? ( - {`${days} bonus reward`} - ) : ( -
Bonus bonding reward
- )} - {!isMobile && ( -

- - This pool bonding over {days ?? "0"} will earn additional bonding - incentives for {remainingEpochs ?? "0"} days. - -

- )} -

- - {isMobile ? "Bonus:" : "Remaining:"} {bonusValue ?? "0"} - -

- {isMobile && remainingEpochs && !isLoading && ( - - {remainingEpochs} days remaining - - )} -
-); diff --git a/packages/web/components/cards/pool-gauge.tsx b/packages/web/components/cards/pool-gauge.tsx deleted file mode 100644 index e3bc28b063..0000000000 --- a/packages/web/components/cards/pool-gauge.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { FunctionComponent } from "react"; -import classNames from "classnames"; -import { MetricLoader } from "../loaders"; -import { InfoTooltip } from "../tooltip"; -import { LoadingProps, MobileProps } from "../types"; - -export const PoolGaugeCard: FunctionComponent< - { - days?: string; - apr?: string; - superfluidApr?: string; - } & LoadingProps & - MobileProps -> = ({ days, apr, isLoading = false, superfluidApr, isMobile = false }) => ( -
-
- - - {days ?? "0"} unbonding - {superfluidApr && ( - - )} - - - -

- APR {apr ?? "0%"} {superfluidApr ? `+ ${superfluidApr}` : null} -

-
-
-
-); - -const UnbondingPeriodHeader: FunctionComponent = ({ - isMobile = false, - children, -}) => - isMobile ? ( - {children} - ) : ( -
{children}
- ); diff --git a/packages/web/components/cards/superfluid-validator.tsx b/packages/web/components/cards/superfluid-validator.tsx deleted file mode 100644 index a051a55f8f..0000000000 --- a/packages/web/components/cards/superfluid-validator.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { FunctionComponent } from "react"; -import { MobileProps } from "../types"; - -export const SuperfluidValidatorCard: FunctionComponent< - { - validatorName?: string; - validatorImgSrc?: string; - validatorCommission?: string; - inactive?: "inactive" | "jailed"; - delegation: string; - apr: string; - } & MobileProps -> = ({ - validatorName, - validatorImgSrc, - validatorCommission, - inactive, - delegation, - apr, - isMobile = false, -}) => ( -
-
- {!isMobile && ( - <> -
- My Superfluid Validator - My Superfluid Delegation -
-
- - )} -
-
- {validatorImgSrc && ( -
- -
- )} -
- - {validatorName ?? ""} - {inactive - ? inactive === "jailed" - ? " (Jailed)" - : " (Inactive)" - : undefined} - - - Commission - {validatorCommission ?? ""} - -
-
-
- {isMobile ? ( - ~{delegation} - ) : ( -
~{delegation}
- )} - ~{apr} APR -
-
-
-
-); diff --git a/packages/web/components/chart/asset-breakdown.tsx b/packages/web/components/chart/asset-breakdown.tsx new file mode 100644 index 0000000000..e60524e516 --- /dev/null +++ b/packages/web/components/chart/asset-breakdown.tsx @@ -0,0 +1,72 @@ +import { FunctionComponent } from "react"; +import classNames from "classnames"; +import { CoinPretty, Dec, IntPretty } from "@keplr-wallet/unit"; + +const ColorCycle = [ + "bg-ion-500", + "bg-rust-500", + "bg-bullish-600", + "bg-ammelia-600", +]; + +export const AssetBreakdownChart: FunctionComponent<{ + assets: { + amount: CoinPretty; + weight: IntPretty; + }[]; + totalWeight: IntPretty; + colorCycle?: typeof ColorCycle; +}> = ({ assets, totalWeight, colorCycle = ColorCycle }) => { + const assetPercentages = assets.map(({ weight }) => + weight.quo(totalWeight).mul(new Dec(100)) + ); + const gridTemplateColumns = assets.map( + (_, index) => `${assetPercentages[index].toString()}fr` + ); + + return ( +
+ {assets.map(({ amount }, index) => ( +
+
+
+
+ + {amount.currency.coinDenom}:{" "} + {assetPercentages[index].toString()}% + +
+
+ {amount.maxDecimals(0).hideDenom(true).toString()} +
+
+
+
+ ))} +
+ ); +}; diff --git a/packages/web/components/chart/generate-series.ts b/packages/web/components/chart/generate-series.ts index 1b57402d8c..5f824cbc05 100644 --- a/packages/web/components/chart/generate-series.ts +++ b/packages/web/components/chart/generate-series.ts @@ -1,4 +1,4 @@ -import { PointOptionsObject, SeriesPieOptions } from "highcharts"; +import type { PointOptionsObject, SeriesPieOptions } from "highcharts"; import { AppCurrency } from "@keplr-wallet/types"; import { HIGHCHART_GRADIENTS } from "./gradients"; diff --git a/packages/web/components/chart/gradients.ts b/packages/web/components/chart/gradients.ts index 2b0e8c5f22..d4af888498 100644 --- a/packages/web/components/chart/gradients.ts +++ b/packages/web/components/chart/gradients.ts @@ -1,4 +1,4 @@ -import { GradientColorObject } from "highcharts"; +import type { GradientColorObject } from "highcharts"; export const HIGHCHART_GRADIENTS: GradientColorObject[] = [ { diff --git a/packages/web/components/chart/index.ts b/packages/web/components/chart/index.ts index aaf4cc108a..e6990eb99d 100644 --- a/packages/web/components/chart/index.ts +++ b/packages/web/components/chart/index.ts @@ -1,3 +1,5 @@ +export * from "./asset-breakdown"; +export * from "./price-breakdown"; export * from "./generate-series"; export * from "./gradients"; export * from "./pie-chart"; diff --git a/packages/web/components/chart/pie-chart.tsx b/packages/web/components/chart/pie-chart.tsx index ded75ccfd2..6f106cb0cb 100644 --- a/packages/web/components/chart/pie-chart.tsx +++ b/packages/web/components/chart/pie-chart.tsx @@ -1,8 +1,12 @@ -import * as Highcharts from "highcharts"; -import HighchartsReact from "highcharts-react-official"; +import dynamic from "next/dynamic"; +import type { Options } from "highcharts"; import React, { FunctionComponent, useState, useEffect } from "react"; -const defaultOptions: Partial = { +const HighchartsReact = dynamic(() => import("highcharts-react-official"), { + ssr: false, +}); + +const defaultOptions: Partial = { chart: { type: "pie", style: { @@ -60,11 +64,12 @@ const defaultOptions: Partial = { }, }; -export const PieChart: FunctionComponent< - HighchartsReact.Props & { height?: number; width?: number } -> = (props) => { - const [options, setOptions] = - useState>(defaultOptions); +export const PieChart: FunctionComponent<{ + height?: number; + width?: number; + options: Options; +}> = (props) => { + const [options, setOptions] = useState>(defaultOptions); useEffect(() => { if (!props.options) return; setOptions((v) => { @@ -74,5 +79,11 @@ export const PieChart: FunctionComponent< return { ...v, ...props.options }; }); }, [props.options, props.height, props.width]); - return ; + + const [hc, setHc] = useState(null); + useEffect(() => { + import("highcharts").then((hc) => setHc(hc)); + }, []); + + return hc ? : null; }; diff --git a/packages/web/components/chart/price-breakdown.tsx b/packages/web/components/chart/price-breakdown.tsx new file mode 100644 index 0000000000..e763504eae --- /dev/null +++ b/packages/web/components/chart/price-breakdown.tsx @@ -0,0 +1,70 @@ +import { FunctionComponent } from "react"; +import classNames from "classnames"; +import { Dec, IntPretty, PricePretty } from "@keplr-wallet/unit"; + +const ColorCycle = [ + "bg-ion-700", + "bg-osmoverse-400", + "bg-bullish-600", + "bg-ammelia-600", +]; + +export const PriceBreakdownChart: FunctionComponent<{ + prices: { label: string; price: PricePretty }[]; + colorCycle?: typeof ColorCycle; +}> = ({ prices, colorCycle = ColorCycle }) => { + const totalWeight = prices.reduce( + (sum, { price }) => sum.add(price), + new IntPretty(0) + ); + + if (totalWeight.toDec().isZero()) return null; + + const positivePrices = prices.filter(({ price }) => !price.toDec().isZero()); + + const assetPercentages = positivePrices.map(({ price }) => + price.quo(totalWeight).mul(new Dec(100)) + ); + const gridTemplateColumns = positivePrices.map( + (_, index) => + `${assetPercentages?.[index].toDec().round().toString() ?? "0"}fr` + ); + + return ( +
+ {positivePrices.map(({ price, label }, index) => { + const percentage = assetPercentages?.[index]; + + if (!percentage) return null; + + return ( +
+
+ {label} +
+ {price.maxDecimals(0).toString()} +
+
+
+
+ ); + })} +
+ ); +}; diff --git a/packages/web/components/complex/add-liquidity.tsx b/packages/web/components/complex/add-liquidity.tsx new file mode 100644 index 0000000000..1c867bf847 --- /dev/null +++ b/packages/web/components/complex/add-liquidity.tsx @@ -0,0 +1,216 @@ +import { FunctionComponent, ReactNode } from "react"; +import classNames from "classnames"; +import { observer } from "mobx-react-lite"; +import { PricePretty, CoinPretty } from "@keplr-wallet/unit"; +import { ObservableAddLiquidityConfig } from "@osmosis-labs/stores"; +import { useStore } from "../../stores"; +import { useWindowSize } from "../../hooks"; +import { MenuToggle } from "../../components/control"; +import { Token } from "../../components/assets"; +import { InputBox } from "../../components/input"; +import { Info } from "../../components/alert"; +import { PoolTokenSelect } from "../../components/control/pool-token-select"; +import { CustomClasses } from "../types"; +import { BorderButton } from "../buttons"; +import { useTranslation } from "react-multi-lang"; + +export const AddLiquidity: FunctionComponent< + { + addLiquidityConfig: ObservableAddLiquidityConfig; + actionButton: ReactNode; + getFiatValue?: (coin: CoinPretty) => PricePretty | undefined; + } & CustomClasses +> = observer( + ({ className, addLiquidityConfig, actionButton, getFiatValue }) => { + const { chainStore } = useStore(); + const { isMobile } = useWindowSize(); + const t = useTranslation(); + + return ( +
+
+
+ { + if (id === "single") { + addLiquidityConfig.setIsSingleAmountIn(true); + } else addLiquidityConfig.setIsSingleAmountIn(false); + }} + /> +
+ {addLiquidityConfig.isSingleAmountIn && ( + {t("addLiquidity.autoswapCaption")} + )} +
+
+ {(addLiquidityConfig.isSingleAmountIn && + addLiquidityConfig.singleAmountInAsset + ? [addLiquidityConfig.singleAmountInAsset] + : addLiquidityConfig.poolAssets + ).map(({ weightFraction, currency }, index) => { + // amount text box value + const inputAmount = addLiquidityConfig.isSingleAmountIn + ? addLiquidityConfig.singleAmountInConfig?.amount ?? "0" + : addLiquidityConfig.getAmountAt(index); + const onInputAmount = (value: string) => { + if (addLiquidityConfig.isSingleAmountIn) { + addLiquidityConfig.singleAmountInConfig?.setAmount(value); + } else { + addLiquidityConfig.setAmountAt(index, value); + } + }; + + const inputAmountValue = + inputAmount !== "" && !isNaN(parseFloat(inputAmount)) + ? getFiatValue?.( + new CoinPretty(currency, inputAmount).moveDecimalPointRight( + currency.coinDecimals + ) + ) + : undefined; + const networkName = chainStore.getChainFromCurrency( + currency.coinDenom + )?.chainName; + const assetBalance = addLiquidityConfig.isSingleAmountIn + ? addLiquidityConfig.singleAmountInBalance + : addLiquidityConfig.getSenderBalanceAt(index); + + const isPeggedCurrency = + typeof currency.originCurrency !== "undefined" && + typeof currency.originCurrency.pegMechanism !== "undefined"; + + return ( +
+ {isPeggedCurrency && ( + + currency.originCurrency!.pegMechanism!.startsWith(vowel) + ) + ? "an" + : "a" + } ${ + currency.originCurrency!.pegMechanism + }-backed stablecoin.`} + /> + )} +
+ {addLiquidityConfig.isSingleAmountIn ? ( + ({ + coinDenom: poolAsset.currency.coinDenom, + networkName: chainStore.getChainFromCurrency( + poolAsset.currency.coinDenom + )?.chainName, + poolShare: poolAsset.weightFraction, + }) + )} + selectedTokenDenom={ + addLiquidityConfig.singleAmountInAsset?.currency + .coinDenom ?? "" + } + onSelectToken={(tokenIndex) => + addLiquidityConfig.setSingleAmountInConfigIndex( + tokenIndex + ) + } + isMobile={isMobile} + /> + ) : ( + + )} +
+ {!isMobile && ( +
+ + {t("addLiquidity.available")} + + {assetBalance && ( + addLiquidityConfig.setMax()} + > + {assetBalance.maxDecimals(6).toString()} + + )} +
+ )} +
+
+ + {!isMobile && ( + + {!inputAmountValue || + inputAmountValue.toDec().isZero() ? ( +
+ ) : ( + `~${inputAmountValue.toString()}` + )} +
+ )} +
+ {isMobile && ( + addLiquidityConfig.setMax()} + > + {t("components.MAX")} + + )} +
+
+
+
+ ); + })} +
+ {addLiquidityConfig.singleAmountInPriceImpact && ( +
+ Price impact + + {addLiquidityConfig.singleAmountInPriceImpact.toString()} + +
+ )} + {actionButton} +
+ ); + } +); diff --git a/packages/web/components/complex/all-pools-table-set.tsx b/packages/web/components/complex/all-pools-table-set.tsx index c2d3543eb9..92944480d4 100644 --- a/packages/web/components/complex/all-pools-table-set.tsx +++ b/packages/web/components/complex/all-pools-table-set.tsx @@ -1,5 +1,4 @@ -import { Dec, PricePretty } from "@keplr-wallet/unit"; -import { ObservablePoolWithFeeMetrics } from "@osmosis-labs/stores"; +import { Dec, PricePretty, RatePretty } from "@keplr-wallet/unit"; import { observer } from "mobx-react-lite"; import { FunctionComponent, @@ -9,6 +8,8 @@ import { useEffect, useRef, } from "react"; +import EventEmitter from "eventemitter3"; +import { ObservableQueryPool } from "@osmosis-labs/stores"; import { EventName } from "../../config"; import { useFilteredData, @@ -18,47 +19,82 @@ import { useAmplitudeAnalytics, } from "../../hooks"; import { useStore } from "../../stores"; -import { Switch, MenuToggle, PageList, SortMenu } from "../control"; +import { Switch, MenuToggle, PageList, SortMenu, MenuOption } from "../control"; import { SearchBox } from "../input"; -import { RowDef, Table } from "../table"; -import { MetricLoaderCell, PoolCompositionCell } from "../table/cells"; +import { ColumnDef, RowDef, Table } from "../table"; +import { + MetricLoaderCell, + PoolCompositionCell, + PoolQuickActionCell, +} from "../table/cells"; import { Breakpoint } from "../types"; import { CompactPoolTableDisplay } from "./compact-pool-table-display"; import { POOLS_PER_PAGE } from "."; +import { useTranslation } from "react-multi-lang"; -const poolsMenuOptions = [ - { id: "incentivized-pools", display: "Incentivized Pools" }, - { id: "all-pools", display: "All Pools" }, -]; +type PoolCell = PoolCompositionCell & MetricLoaderCell & PoolQuickActionCell; const TVL_FILTER_THRESHOLD = 1000; +type PoolWithMetrics = { + pool: ObservableQueryPool; + liquidity: PricePretty; + myLiquidity: PricePretty; + myAvailableLiquidity: PricePretty; + apr?: RatePretty; + poolName: string; + networkNames: string; + volume24h: PricePretty; + volume7d: PricePretty; + feesSpent24h: PricePretty; + feesSpent7d: PricePretty; + feesPercentage: string; +}; + export const AllPoolsTableSet: FunctionComponent<{ tableSet?: "incentivized-pools" | "all-pools"; -}> = observer(({ tableSet = "incentivized-pools" }) => { - const { - chainStore, - queriesExternalStore, - priceStore, - queriesStore, - accountStore, - } = useStore(); - const { isMobile } = useWindowSize(); - const { logEvent } = useAmplitudeAnalytics(); + quickAddLiquidity: (poolId: string) => void; + quickRemoveLiquidity: (poolId: string) => void; + quickLockTokens: (poolId: string) => void; +}> = observer( + ({ + tableSet = "incentivized-pools", + quickAddLiquidity, + quickRemoveLiquidity, + quickLockTokens, + }) => { + const { + chainStore, + queriesExternalStore, + priceStore, + queriesStore, + accountStore, + } = useStore(); + const { isMobile } = useWindowSize(); + const t = useTranslation(); - const [activeOptionId, setActiveOptionId] = useState(tableSet); - const selectOption = (optionId: string) => { - if (optionId === "incentivized-pools" || optionId === "all-pools") { - setActiveOptionId(optionId); - } - }; - const [isPoolTvlFiltered, do_setIsPoolTvlFiltered] = useState(false); - const tvlFilterLabel = `Show pools less than ${new PricePretty( - priceStore.getFiatCurrency(priceStore.defaultVsCurrency)!, - TVL_FILTER_THRESHOLD - ).toString()}`; - const setIsPoolTvlFiltered = useCallback( - (isFiltered: boolean) => { + const { logEvent } = useAmplitudeAnalytics(); + + const [activeOptionId, setActiveOptionId] = useState(tableSet); + + const poolsMenuOptions = [ + { id: "incentivized-pools", display: t("pools.incentivized") }, + { id: "all-pools", display: t("pools.all") }, + ]; + + const selectOption = (optionId: string) => { + if (optionId === "incentivized-pools" || optionId === "all-pools") { + setActiveOptionId(optionId); + } + }; + const [isPoolTvlFiltered, do_setIsPoolTvlFiltered] = useState(false); + const tvlFilterLabel = t("pools.allPools.displayLowLiquidity", { + value: new PricePretty( + priceStore.getFiatCurrency(priceStore.defaultVsCurrency)!, + TVL_FILTER_THRESHOLD + ).toString(), + }); + const setIsPoolTvlFiltered = useCallback((isFiltered: boolean) => { logEvent([ EventName.Pools.allPoolsListFiltered, { @@ -67,480 +103,550 @@ export const AllPoolsTableSet: FunctionComponent<{ }, ]); do_setIsPoolTvlFiltered(isFiltered); - }, - [do_setIsPoolTvlFiltered] - ); + }, []); - const { chainId } = chainStore.osmosis; - const queriesOsmosis = queriesStore.get(chainId).osmosis!; - const queriesExternal = queriesExternalStore.get(); - const account = accountStore.getAccount(chainId); - const fiat = priceStore.getFiatCurrency(priceStore.defaultVsCurrency)!; + const { chainId } = chainStore.osmosis; + const queriesOsmosis = queriesStore.get(chainId).osmosis!; + const account = accountStore.getAccount(chainId); + const fiat = priceStore.getFiatCurrency(priceStore.defaultVsCurrency)!; - const allPools = queriesOsmosis.queryGammPools.getAllPools(); - const incentivizedPoolIds = - queriesOsmosis.queryIncentivizedPools.incentivizedPools; + const allPools = queriesOsmosis.queryGammPools.getAllPools(); - const allPoolsWithMetrics = useMemo( - () => - allPools.map((pool) => ({ - ...queriesExternal.queryGammPoolFeeMetrics.makePoolWithFeeMetrics( - pool, - priceStore - ), - myLiquidity: pool - .computeTotalValueLocked(priceStore) - .mul( + const allPoolsWithMetrics: PoolWithMetrics[] = useMemo( + () => + allPools.map((pool) => { + const poolTvl = pool.computeTotalValueLocked(priceStore); + const myLiquidity = poolTvl.mul( queriesOsmosis.queryGammPoolShare.getAllGammShareRatio( account.bech32Address, pool.id ) - ), - apr: queriesOsmosis.queryIncentivizedPools - .computeMostAPY(pool.id, priceStore) - .maxDecimals(2), - poolName: pool.poolAssets - .map((asset) => asset.amount.currency.coinDenom) - .join("/"), - networkNames: pool.poolAssets - .map( - (asset) => - chainStore.getChainFromCurrency(asset.amount.denom)?.chainName ?? - "" + ); + + return { + pool, + ...queriesExternalStore.queryGammPoolFeeMetrics.getPoolFeesMetrics( + pool.id, + priceStore + ), + liquidity: pool.computeTotalValueLocked(priceStore), + myLiquidity, + myAvailableLiquidity: myLiquidity.toDec().isZero() + ? new PricePretty(fiat, 0) + : poolTvl.mul( + queriesOsmosis.queryGammPoolShare + .getAvailableGammShare(account.bech32Address, pool.id) + .quo(pool.totalShare) + ), + poolName: pool.poolAssets + .map((asset) => asset.amount.currency.coinDenom) + .join("/"), + networkNames: pool.poolAssets + .map( + (asset) => + chainStore.getChainFromCurrency(asset.amount.denom) + ?.chainName ?? "" + ) + .join(" "), + }; + }), + [ + // note: mobx only causes rerenders for values referenced *during* render. I.e. *not* within useEffect/useCallback/useMemo hooks (see: https://mobx.js.org/react-integration.html) + // `useMemo` is needed in this file to avoid "debounce" with the hundreds of re-renders by mobx as the 200+ API requests come in and populate 1000+ observables (otherwise the UI is unresponsive for 30+ seconds) + // also, the higher level `useMemo`s (i.e. this one) gain the most performance as other React renders are prevented down the line as data is calculated (remember, renders are initiated by both mobx and react) + allPools, + queriesOsmosis.queryGammPools.response, + queriesExternalStore.queryGammPoolFeeMetrics.response, + queriesOsmosis.queryAccountLocked.get(account.bech32Address).response, + queriesOsmosis.queryLockedCoins.get(account.bech32Address).response, + queriesOsmosis.queryUnlockingCoins.get(account.bech32Address).response, + priceStore.response, + queriesExternalStore.queryGammPoolFeeMetrics.response, + account.bech32Address, + ] + ); + + const incentivizedPoolsWithMetrics = allPoolsWithMetrics.reduce( + ( + incentivizedPools: PoolWithMetrics[], + poolWithMetrics: PoolWithMetrics + ) => { + if ( + queriesOsmosis.queryIncentivizedPools.incentivizedPools.some( + (incentivizedPoolId) => + poolWithMetrics.pool.id === incentivizedPoolId ) - .join(" "), - })), - [ - allPools, - account.bech32Address, - queriesOsmosis, - priceStore, - queriesExternal, - queriesExternal.queryGammPoolFeeMetrics.response, - ] - ); + ) { + incentivizedPools.push({ + ...poolWithMetrics, + apr: queriesOsmosis.queryIncentivizedPools + .computeMostApr(poolWithMetrics.pool.id, priceStore) + .add( + // swap fees + queriesExternalStore.queryGammPoolFeeMetrics.get7dPoolFeeApr( + poolWithMetrics.pool, + priceStore + ) + ) + .add( + // superfluid apr + queriesOsmosis.querySuperfluidPools.isSuperfluidPool( + poolWithMetrics.pool.id + ) + ? new RatePretty( + queriesStore + .get(chainId) + .cosmos.queryInflation.inflation.mul( + queriesOsmosis.querySuperfluidOsmoEquivalent.estimatePoolAPROsmoEquivalentMultiplier( + poolWithMetrics.pool.id + ) + ) + .moveDecimalPointLeft(2) + ) + : new Dec(0) + ) + .maxDecimals(0), + }); + } + return incentivizedPools; + }, + [] + ); - const incentivizedPoolsWithMetrics = useMemo( - () => - allPoolsWithMetrics.reduce( - ( - incentivizedPools: ObservablePoolWithFeeMetrics[], - poolWithMetrics: ObservablePoolWithFeeMetrics - ) => { - if ( - incentivizedPoolIds.some( - (incentivizedPoolId) => - poolWithMetrics.pool.id === incentivizedPoolId - ) - ) { - incentivizedPools.push(poolWithMetrics); - } - return incentivizedPools; - }, - [] - ), - [allPoolsWithMetrics, incentivizedPoolIds] - ); + const isIncentivizedPools = activeOptionId === poolsMenuOptions[0].id; + const activeOptionPools = useMemo( + () => + isIncentivizedPools + ? incentivizedPoolsWithMetrics + : allPoolsWithMetrics, + [isIncentivizedPools, incentivizedPoolsWithMetrics, allPoolsWithMetrics] + ); - const isIncentivizedPools = activeOptionId === poolsMenuOptions[0].id; - const activeOptionPools = useMemo( - () => - isIncentivizedPools ? incentivizedPoolsWithMetrics : allPoolsWithMetrics, - [isIncentivizedPools, incentivizedPoolsWithMetrics, allPoolsWithMetrics] - ); + const tvlFilteredPools = useMemo(() => { + return isPoolTvlFiltered + ? activeOptionPools + : activeOptionPools.filter((poolWithMetrics) => + poolWithMetrics.liquidity.toDec().gte(new Dec(TVL_FILTER_THRESHOLD)) + ); + }, [ + isPoolTvlFiltered, + activeOptionPools, + queriesExternalStore.queryGammPoolFeeMetrics.response, + ]); - const tvlFilteredPools = useMemo(() => { - return isPoolTvlFiltered - ? activeOptionPools - : activeOptionPools.filter((poolWithMetrics) => - poolWithMetrics.liquidity.toDec().gte(new Dec(TVL_FILTER_THRESHOLD)) - ); - }, [isPoolTvlFiltered, activeOptionPools]); + const initialKeyPath = "liquidity"; + const initialSortDirection = "descending"; + const [ + sortKeyPath, + setSortKeyPath, + sortDirection, + setSortDirection, + toggleSortDirection, + sortedAllPoolsWithMetrics, + ] = useSortedData(tvlFilteredPools, initialKeyPath, initialSortDirection); - const initialKeyPath = "liquidity"; - const initialSortDirection = "descending"; - const [ - sortKeyPath, - setSortKeyPath, - sortDirection, - setSortDirection, - toggleSortDirection, - sortedAllPoolsWithMetrics, - ] = useSortedData(tvlFilteredPools, initialKeyPath, initialSortDirection); + const [query, setQuery, filteredPools] = useFilteredData( + sortedAllPoolsWithMetrics, + [ + "pool.id", + "poolName", + "networkNames", + "pool.poolAssets.amount.currency.originCurrency.pegMechanism", + ] + ); - const [query, setQuery, filteredPools] = useFilteredData( - sortedAllPoolsWithMetrics, - [ - "pool.id", - "poolName", - "networkNames", - "pool.poolAssets.amount.currency.originCurrency.pegMechanism", - ] - ); + const [page, setPage, minPage, numPages, allData] = usePaginatedData( + filteredPools, + POOLS_PER_PAGE + ); - const [page, setPage, minPage, numPages, allData] = usePaginatedData( - filteredPools, - POOLS_PER_PAGE - ); - const makeSortMechanism = useCallback( - (keyPath: string) => - sortKeyPath === keyPath - ? { - currentDirection: sortDirection, - onClickHeader: () => { - switch (sortDirection) { - case "ascending": - const newSortDirection = "descending"; - logEvent([ - EventName.Pools.allPoolsListSorted, - { - sortedBy: keyPath, - sortDirection: newSortDirection, - sortedOn: "table", - }, - ]); - setSortDirection(newSortDirection); - break; - case "descending": - // default sort key toggles forever - if (sortKeyPath === initialKeyPath) { - const newSortDirection = "ascending"; + const makeSortMechanism = useCallback( + (keyPath: string) => + sortKeyPath === keyPath + ? { + currentDirection: sortDirection, + onClickHeader: () => { + switch (sortDirection) { + case "ascending": logEvent([ EventName.Pools.allPoolsListSorted, { sortedBy: keyPath, - sortDirection: newSortDirection, - + sortDirection: "descending", sortedOn: "table", }, ]); - setSortDirection(newSortDirection); - } else { - // other keys toggle then go back to default - setSortKeyPath(initialKeyPath); - setSortDirection(initialSortDirection); - } - } - }, - } - : { - onClickHeader: () => { - const newSortDirection = "ascending"; - logEvent([ - EventName.Pools.allPoolsListSorted, - { - sortedBy: keyPath, - sortDirection: newSortDirection, + setSortDirection("descending"); + break; + case "descending": + // default sort key toggles forever + if (sortKeyPath === initialKeyPath) { + logEvent([ + EventName.Pools.allPoolsListSorted, + { + sortedBy: keyPath, + sortDirection: "ascending", - sortedOn: "table", - }, - ]); - setSortKeyPath(keyPath); - setSortDirection(newSortDirection); + sortedOn: "table", + }, + ]); + setSortDirection("ascending"); + } else { + // other keys toggle then go back to default + setSortKeyPath(initialKeyPath); + setSortDirection(initialSortDirection); + } + } + }, + } + : { + onClickHeader: () => { + const newSortDirection = "ascending"; + logEvent([ + EventName.Pools.allPoolsListSorted, + { + sortedBy: keyPath, + sortDirection: newSortDirection, + + sortedOn: "table", + }, + ]); + setSortKeyPath(keyPath); + setSortDirection(newSortDirection); + }, }, + [sortKeyPath, sortDirection] + ); + const tableCols: ColumnDef[] = useMemo( + () => [ + { + id: "pool.id", + display: t("pools.allPools.sort.poolName"), + sort: makeSortMechanism("pool.id"), + displayCell: PoolCompositionCell, + }, + { + id: "liquidity", + display: t("pools.allPools.sort.liquidity"), + sort: makeSortMechanism("liquidity"), + }, + { + id: "volume24h", + display: t("pools.allPools.sort.volume24h"), + sort: makeSortMechanism("volume24h"), + displayCell: MetricLoaderCell, + }, + { + id: "feesSpent7d", + display: t("pools.allPools.sort.fees"), + sort: makeSortMechanism("feesSpent7d"), + displayCell: MetricLoaderCell, + collapseAt: Breakpoint.XL, + }, + { + id: isIncentivizedPools ? "apr" : "myLiquidity", + display: isIncentivizedPools + ? t("pools.allPools.sort.APRIncentivized") + : t("pools.allPools.sort.APR"), + sort: makeSortMechanism(isIncentivizedPools ? "apr" : "myLiquidity"), + displayCell: isIncentivizedPools ? MetricLoaderCell : undefined, + collapseAt: Breakpoint.LG, + }, + { id: "quickActions", display: "", displayCell: PoolQuickActionCell }, + ], + [isIncentivizedPools, t] + ); + + const tableRows: RowDef[] = useMemo( + () => + allData.map((poolWithFeeMetrics) => ({ + link: `/pool/${poolWithFeeMetrics.pool.id}`, + onClick: () => { + logEvent([ + isIncentivizedPools + ? EventName.Pools.incentivizedPoolsItemClicked + : EventName.Pools.allPoolsItemClicked, + { + poolId: poolWithFeeMetrics.pool.id, + poolName: poolWithFeeMetrics.pool.poolAssets + .map((poolAsset) => poolAsset.amount.denom) + .join(" / "), + poolWeight: poolWithFeeMetrics.pool.poolAssets + .map((poolAsset) => poolAsset.weightFraction.toString()) + .join(" / "), + isSuperfluidPool: + queriesOsmosis.querySuperfluidPools.isSuperfluidPool( + poolWithFeeMetrics.pool.id + ), + }, + ]); }, - [sortKeyPath, sortDirection, setSortDirection, setSortKeyPath] - ); - const tableCols = useMemo( - () => [ - { - id: "pool.id", - display: "Pool Name", - sort: makeSortMechanism("pool.id"), - displayCell: PoolCompositionCell, - }, - { - id: "liquidity", - display: "Liquidity", - sort: makeSortMechanism("liquidity"), - }, - { - id: "volume24h", - display: "Volume (24H)", - sort: makeSortMechanism("volume24h"), + })), + [allData] + ); - displayCell: MetricLoaderCell, - }, - { - id: "feesSpent7d", - display: "Fees (7D)", - sort: makeSortMechanism("feesSpent7d"), - displayCell: MetricLoaderCell, - collapseAt: Breakpoint.XL, - }, - { - id: isIncentivizedPools ? "apr" : "myLiquidity", - display: isIncentivizedPools ? "APR" : "My Liquidity", - sort: makeSortMechanism(isIncentivizedPools ? "apr" : "myLiquidity"), - displayCell: isIncentivizedPools ? MetricLoaderCell : undefined, - collapseAt: Breakpoint.LG, - }, - ], - [makeSortMechanism, isIncentivizedPools] - ); + const [cellGroupEventEmitter] = useState(() => new EventEmitter()); + const tableData = useMemo( + () => + allData.map((poolWithMetrics) => { + const poolId = poolWithMetrics.pool.id; + const poolAssets = poolWithMetrics.pool.poolAssets.map( + (poolAsset) => ({ + coinImageUrl: poolAsset.amount.currency.coinImageUrl, + coinDenom: poolAsset.amount.currency.coinDenom, + }) + ); - const tableRows: RowDef[] = useMemo( - () => - allData.map((poolWithFeeMetrics) => ({ - link: `/pool/${poolWithFeeMetrics.pool.id}`, - onClick: () => { - logEvent([ - isIncentivizedPools - ? EventName.Pools.incentivizedPoolsItemClicked - : EventName.Pools.allPoolsItemClicked, + return [ { - poolId: poolWithFeeMetrics.pool.id, - poolName: poolWithFeeMetrics.pool.poolAssets - .map((poolAsset) => poolAsset.amount.denom) - .join(" / "), - poolWeight: poolWithFeeMetrics.pool.poolAssets - .map((poolAsset) => poolAsset.weightFraction.toString()) - .join(" / "), - isSuperfluidPool: - queriesOsmosis.querySuperfluidPools.isSuperfluidPool( - poolWithFeeMetrics.pool.id - ), + poolId, + poolAssets, }, - ]); - }, - })), - [allData] - ); - - const tableData = useMemo( - () => - allData.map((poolWithMetrics) => { - const poolId = poolWithMetrics.pool.id; - const poolAssets = poolWithMetrics.pool.poolAssets.map((poolAsset) => ({ - coinImageUrl: poolAsset.amount.currency.coinImageUrl, - coinDenom: poolAsset.amount.currency.coinDenom, - })); + { value: poolWithMetrics.liquidity.toString() }, + { + value: poolWithMetrics.volume24h.toString(), + isLoading: !queriesExternalStore.queryGammPoolFeeMetrics.response, + }, + { + value: poolWithMetrics.feesSpent7d.toString(), + isLoading: !queriesExternalStore.queryGammPoolFeeMetrics.response, + }, + { + value: isIncentivizedPools + ? poolWithMetrics.apr?.toString() + : poolWithMetrics.myLiquidity?.toString(), + isLoading: isIncentivizedPools + ? queriesOsmosis.queryIncentivizedPools.isAprFetching + : false, + }, + { + poolId, + cellGroupEventEmitter, + onAddLiquidity: () => quickAddLiquidity(poolId), + onRemoveLiquidity: !poolWithMetrics.myAvailableLiquidity + .toDec() + .isZero() + ? () => quickRemoveLiquidity(poolId) + : undefined, + onLockTokens: !poolWithMetrics.myAvailableLiquidity + .toDec() + .isZero() + ? () => quickLockTokens(poolId) + : undefined, + }, + ]; + }), + [ + allData, + isIncentivizedPools, + queriesOsmosis.queryIncentivizedPools.isAprFetching, + ] + ); - return [ - { - poolId, - poolAssets, - isIncentivized: incentivizedPoolIds.some((id) => id === poolId), - }, - { value: poolWithMetrics.liquidity.toString() }, - { - value: poolWithMetrics.volume24h.toString(), - isLoading: !queriesExternal.queryGammPoolFeeMetrics.response, - }, - { - value: poolWithMetrics.feesSpent7d.toString(), - isLoading: !queriesExternal.queryGammPoolFeeMetrics.response, - }, - { - value: isIncentivizedPools - ? poolWithMetrics.apr?.toString() - : poolWithMetrics.myLiquidity?.toString(), - isLoading: isIncentivizedPools - ? queriesOsmosis.queryIncentivizedPools.isAprFetching - : false, - }, - ]; - }), - [ - allData, - isIncentivizedPools, - incentivizedPoolIds, - queriesExternal, - queriesOsmosis, - ] - ); + // auto expand searchable pools set when user is actively searching + const didAutoSwitchActiveSet = useRef(false); + const didAutoSwitchTVLFilter = useRef(false); + useEffect(() => { + // first expand to all pools, then to low TVL pools + // remember if/what we switched for user + if (query !== "" && filteredPools.length < POOLS_PER_PAGE) { + if (activeOptionId === "all-pools") { + if (!isPoolTvlFiltered) didAutoSwitchTVLFilter.current = true; + setIsPoolTvlFiltered(true); + } else { + if (activeOptionId === "incentivized-pools") + didAutoSwitchActiveSet.current = true; + setActiveOptionId("all-pools"); + } + } - // auto expand searchable pools set when user is actively searching - const didAutoSwitchActiveSet = useRef(false); - const didAutoSwitchTVLFilter = useRef(false); - useEffect(() => { - // first expand to all pools, then to low TVL pools - // remember if/what we switched for user - if (query !== "" && filteredPools.length < POOLS_PER_PAGE) { - if (activeOptionId === "all-pools") { - if (!isPoolTvlFiltered) didAutoSwitchTVLFilter.current = true; - setIsPoolTvlFiltered(true); - } else { - if (activeOptionId === "incentivized-pools") - didAutoSwitchActiveSet.current = true; - setActiveOptionId("all-pools"); + // reset filter states when query cleared only if auto switched + if (query === "" && didAutoSwitchActiveSet.current) { + setActiveOptionId("incentivized-pools"); + didAutoSwitchActiveSet.current = false; } - } + if (query === "" && didAutoSwitchTVLFilter.current) { + setIsPoolTvlFiltered(false); + didAutoSwitchTVLFilter.current = false; + } + }, [query, filteredPools, isPoolTvlFiltered, activeOptionId]); - // reset filter states when query cleared only if auto switched - if (query === "" && didAutoSwitchActiveSet.current) { - setActiveOptionId("incentivized-pools"); - didAutoSwitchActiveSet.current = false; - } - if (query === "" && didAutoSwitchTVLFilter.current) { - setIsPoolTvlFiltered(false); - didAutoSwitchTVLFilter.current = false; + if (isMobile) { + return ( + ({ + id: poolData.pool.id, + assets: poolData.pool.poolAssets.map( + ({ + amount: { + currency: { coinDenom, coinImageUrl }, + }, + }) => ({ + coinDenom, + coinImageUrl, + }) + ), + metrics: [ + ...[ + sortKeyPath === "volume24h" + ? { + label: t("pools.allPools.sort.volume24h"), + value: poolData.volume24h.toString(), + } + : sortKeyPath === "feesSpent7d" + ? { + label: t("pools.allPools.sort.fees"), + value: poolData.feesSpent7d.toString(), + } + : sortKeyPath === "apr" + ? { + label: t("pools.allPools.sort.APRIncentivized"), + value: poolData.apr?.toString() ?? "0%", + } + : sortKeyPath === "myLiquidity" + ? { + label: t("pools.allPools.myLiquidity"), + value: + poolData.myLiquidity?.toString() ?? `0${fiat.symbol}`, + } + : { + label: t("pools.allPools.TVL"), + value: poolData.liquidity.toString(), + }, + ], + ...[ + sortKeyPath === "apr" + ? { + label: t("pools.allPools.TVL"), + value: poolData.liquidity.toString(), + } + : { + label: isIncentivizedPools + ? t("pools.allPools.APR") + : t("pools.allPools.APRIncentivized"), + value: isIncentivizedPools + ? poolData.apr?.toString() ?? "0%" + : poolData.volume7d.toString(), + }, + ], + ], + isSuperfluidPool: + queriesOsmosis.querySuperfluidPools.isSuperfluidPool( + poolData.pool.id + ), + }))} + searchBoxProps={{ + currentValue: query, + onInput: setQuery, + placeholder: t("pools.allPools.search"), + }} + sortMenuProps={{ + options: tableCols.filter( + (col) => + typeof col.display === "string" && col.display.length !== 0 + ) as MenuOption[], + selectedOptionId: sortKeyPath, + onSelect: (id) => + id === sortKeyPath ? setSortKeyPath("") : setSortKeyPath(id), + onToggleSortDirection: toggleSortDirection, + }} + pageListProps={{ + currentValue: page, + max: numPages, + min: minPage, + onInput: setPage, + }} + minTvlToggleProps={{ + isOn: isPoolTvlFiltered, + onToggle: setIsPoolTvlFiltered, + label: tvlFilterLabel, + }} + /> + ); } - }, [ - query, - filteredPools, - isPoolTvlFiltered, - activeOptionId, - setIsPoolTvlFiltered, - setActiveOptionId, - ]); - if (isMobile) { return ( - ({ - id: poolData.pool.id, - assets: poolData.pool.poolAssets.map( - ({ - amount: { - currency: { coinDenom, coinImageUrl }, - }, - }) => ({ - coinDenom, - coinImageUrl, - }) - ), - metrics: [ - ...[ - sortKeyPath === "volume24h" - ? { - label: "", - value: poolData.volume24h.toString(), - } - : sortKeyPath === "feesSpent7d" - ? { label: "", value: poolData.feesSpent7d.toString() } - : sortKeyPath === "apr" - ? { label: "", value: poolData.apr?.toString() ?? "0%" } - : sortKeyPath === "myLiquidity" - ? { - label: "my liquidity", - value: - poolData.myLiquidity?.toString() ?? `0${fiat.symbol}`, - } - : { label: "TVL", value: poolData.liquidity.toString() }, - ], - ...[ - sortKeyPath === "apr" - ? { label: "TVL", value: poolData.liquidity.toString() } - : { - label: isIncentivizedPools ? "APR" : "7d Vol.", - value: isIncentivizedPools - ? poolData.apr?.toString() ?? "0%" - : poolData.volume7d.toString(), - }, - ], - ], - isSuperfluidPool: - queriesOsmosis.querySuperfluidPools.isSuperfluidPool( - poolData.pool.id - ), - }))} - searchBoxProps={{ - currentValue: query, - onInput: setQuery, - placeholder: "Search pools", - }} - sortMenuProps={{ - options: tableCols, - selectedOptionId: sortKeyPath, - onSelect: (id) => - id === sortKeyPath ? setSortKeyPath("") : setSortKeyPath(id), - onToggleSortDirection: toggleSortDirection, - }} - pageListProps={{ - currentValue: page, - max: numPages, - min: minPage, - onInput: setPage, - }} - minTvlToggleProps={{ - isOn: isPoolTvlFiltered, - onToggle: setIsPoolTvlFiltered, - label: tvlFilterLabel, - }} - /> - ); - } - - return ( - <> -
-
-
All Pools
- -
-
- - {tvlFilterLabel} - -
- +
+
+
{t("pools.allPools.title")}
+ + + {tvlFilterLabel} + + +
+
+ - { - if (id === sortKeyPath) { - setSortKeyPath(""); - } else { +
+ + { + if (id === sortKeyPath) { + setSortKeyPath(""); + } else { + logEvent([ + EventName.Pools.allPoolsListSorted, + { + sortedBy: id, + sortDirection: sortDirection, + sortedOn: "dropdown", + }, + ]); + setSortKeyPath(id); + } + }} + onToggleSortDirection={() => { logEvent([ EventName.Pools.allPoolsListSorted, { - sortedBy: id, - sortDirection: sortDirection, + sortedBy: sortKeyPath, + sortDirection: + sortDirection === "ascending" + ? "descending" + : "ascending", sortedOn: "dropdown", }, ]); - setSortKeyPath(id); - } - }} - onToggleSortDirection={() => { - logEvent([ - EventName.Pools.allPoolsListSorted, - { - sortedBy: sortKeyPath, - sortDirection: - sortDirection === "ascending" - ? "descending" - : "ascending", - sortedOn: "dropdown", - }, - ]); - toggleSortDirection(); - }} - /> + toggleSortDirection(); + }} + /> +
-
- - className="mt-5 w-full lg:text-sm" - columnDefs={tableCols} - rowDefs={tableRows} - data={tableData} - /> -
- + className="my-5 w-full lg:text-sm" + columnDefs={tableCols} + rowDefs={tableRows} + data={tableData} /> -
- - ); -}); +
+ +
+ + ); + } +); diff --git a/packages/web/components/complex/compact-pool-table-display.tsx b/packages/web/components/complex/compact-pool-table-display.tsx index 08a9e4b28b..2eed15a079 100644 --- a/packages/web/components/complex/compact-pool-table-display.tsx +++ b/packages/web/components/complex/compact-pool-table-display.tsx @@ -1,6 +1,5 @@ -import classNames from "classnames"; import { useRouter } from "next/router"; -import { FunctionComponent, ReactElement } from "react"; +import { FunctionComponent } from "react"; import { PoolAssetInfo } from "../assets"; import { AssetCard } from "../cards"; import { @@ -16,7 +15,6 @@ import { InputProps, Metric } from "../types"; /** Stateless component for displaying & filtering/sorting pools on a compact screen. */ export const CompactPoolTableDisplay: FunctionComponent<{ - title: string | ReactElement; pools: { id: string; assets: PoolAssetInfo[]; @@ -29,7 +27,6 @@ export const CompactPoolTableDisplay: FunctionComponent<{ pageListProps?: NumberSelectProps; minTvlToggleProps?: ToggleProps & { label: string }; }> = ({ - title, pools, onClickPoolCard, searchBoxProps, @@ -40,34 +37,28 @@ export const CompactPoolTableDisplay: FunctionComponent<{ const router = useRouter(); return ( -
+
{searchBoxProps && ( - + )} -
- {typeof title === "string" ? ( - {title} - ) : ( - <>{title} +
+ {minTvlToggleProps && ( + + + {minTvlToggleProps.label} + + )} -
- {minTvlToggleProps && ( - {minTvlToggleProps.label} - )} - {sortMenuProps && } -
+ {sortMenuProps && }
{pools.map(({ id, assets, metrics, isSuperfluid }) => ( asset.coinDenom).join("/")} coinImageUrl={assets} metrics={metrics} - coinDenomCaption={`Pool #${id}`} + coinDenomCaption={id} isSuperfluid={isSuperfluid} onClick={() => { if (onClickPoolCard) { diff --git a/packages/web/components/complex/external-incentivized-pools-table-set.tsx b/packages/web/components/complex/external-incentivized-pools-table-set.tsx index db4470f734..fa0e3ec831 100644 --- a/packages/web/components/complex/external-incentivized-pools-table-set.tsx +++ b/packages/web/components/complex/external-incentivized-pools-table-set.tsx @@ -1,7 +1,8 @@ -import { CoinPretty, Dec } from "@keplr-wallet/unit"; +import { CoinPretty, Dec, PricePretty } from "@keplr-wallet/unit"; import { ObservableQueryPool } from "@osmosis-labs/stores"; import { observer } from "mobx-react-lite"; -import { FunctionComponent, useMemo, useCallback } from "react"; +import { FunctionComponent, useMemo, useCallback, useState } from "react"; +import EventEmitter from "eventemitter3"; import { EventName, ExternalIncentiveGaugeAllowList } from "../../config"; import { useFilteredData, @@ -14,13 +15,22 @@ import { useStore } from "../../stores"; import { PageList, SortMenu } from "../control"; import { SearchBox } from "../input"; import { RowDef, Table } from "../table"; -import { MetricLoaderCell, PoolCompositionCell } from "../table/cells"; +import { + MetricLoaderCell, + PoolCompositionCell, + PoolQuickActionCell, +} from "../table/cells"; import { Breakpoint } from "../types"; import { CompactPoolTableDisplay } from "./compact-pool-table-display"; import { POOLS_PER_PAGE } from "."; +import { useTranslation } from "react-multi-lang"; -export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( - () => { +export const ExternalIncentivizedPoolsTableSet: FunctionComponent<{ + quickAddLiquidity: (poolId: string) => void; + quickRemoveLiquidity: (poolId: string) => void; + quickLockTokens: (poolId: string) => void; +}> = observer( + ({ quickAddLiquidity, quickRemoveLiquidity, quickLockTokens }) => { const { chainStore, queriesExternalStore, @@ -30,19 +40,14 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( } = useStore(); const { isMobile } = useWindowSize(); const { logEvent } = useAmplitudeAnalytics(); + const t = useTranslation(); const { chainId } = chainStore.osmosis; - const queryExternal = queriesExternalStore.get(); const queryOsmosis = queriesStore.get(chainId).osmosis!; const account = accountStore.getAccount(chainId); - const pools = Object.keys(ExternalIncentiveGaugeAllowList).map( - (poolId: string) => { - const pool = queryOsmosis.queryGammPools.getPool(poolId); - if (pool) { - return pool; - } - } + const pools = Object.keys(ExternalIncentiveGaugeAllowList).map((poolId) => + queryOsmosis.queryGammPools.getPool(poolId) ); const externalIncentivizedPools = useMemo( @@ -76,7 +81,7 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( return maxRemainingEpoch > 0; } ), - [pools, queryOsmosis] + [pools] ); const externalIncentivizedPoolsWithMetrics = useMemo( @@ -107,23 +112,43 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( } } + const poolTvl = pool.computeTotalValueLocked(priceStore); + const myLiquidity = poolTvl.mul( + queryOsmosis.queryGammPoolShare.getAllGammShareRatio( + account.bech32Address, + pool.id + ) + ); + return { - ...queryExternal.queryGammPoolFeeMetrics.makePoolWithFeeMetrics( - pool, + pool, + ...queriesExternalStore.queryGammPoolFeeMetrics.getPoolFeesMetrics( + pool.id, priceStore ), + liquidity: pool.computeTotalValueLocked(priceStore), epochsRemaining: maxRemainingEpoch, - myLiquidity: pool - .computeTotalValueLocked(priceStore) - .mul( - queryOsmosis.queryGammPoolShare.getAllGammShareRatio( - account.bech32Address, - pool.id + myLiquidity, + myAvailableLiquidity: myLiquidity.toDec().isZero() + ? new PricePretty( + priceStore.getFiatCurrency(priceStore.defaultVsCurrency)!, + 0 ) - ), + : poolTvl.mul( + queryOsmosis.queryGammPoolShare + .getAvailableGammShare(account.bech32Address, pool.id) + .quo(pool.totalShare) + ), apr: queryOsmosis.queryIncentivizedPools - .computeMostAPY(pool.id, priceStore) - .maxDecimals(2), + .computeMostApr(pool.id, priceStore) + .add( + // swap fees + queriesExternalStore.queryGammPoolFeeMetrics.get7dPoolFeeApr( + pool, + priceStore + ) + ) + .maxDecimals(0), poolName: pool.poolAssets .map((asset) => asset.amount.currency.coinDenom) .join("/"), @@ -139,8 +164,9 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( [ chainId, externalIncentivizedPools, - queryOsmosis, - queryExternal, + queryOsmosis.queryIncentivizedPools.response, + queriesExternalStore.queryGammPoolFeeMetrics.response, + queryOsmosis.queryGammPools.response, priceStore, account, chainStore, @@ -233,41 +259,42 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( setSortDirection(newSortDirection); }, }, - [sortKeyPath, sortDirection, setSortDirection, setSortKeyPath] + [sortKeyPath, sortDirection] ); const tableCols = useMemo( () => [ { id: "pool.id", - display: "Pool Name", + display: t("pools.externalIncentivized.sort.poolName"), sort: makeSortMechanism("pool.id"), displayCell: PoolCompositionCell, }, { id: "liquidity", - display: "Liquidity", + display: t("pools.externalIncentivized.sort.liquidity"), sort: makeSortMechanism("liquidity"), }, { id: "apr", - display: "APR", + display: t("pools.externalIncentivized.sort.APR"), sort: makeSortMechanism("apr"), displayCell: MetricLoaderCell, }, { id: "epochsRemaining", - display: "Epochs Remaining", + display: t("pools.externalIncentivized.sort.epochs"), sort: makeSortMechanism("epochsRemaining"), collapseAt: Breakpoint.XL, }, { id: "myLiquidity", - display: "My Liquidity", + display: t("pools.externalIncentivized.sort.myLiquidity"), sort: makeSortMechanism("myLiquidity"), collapseAt: Breakpoint.LG, }, + { id: "quickActions", display: "", displayCell: PoolQuickActionCell }, ], - [makeSortMechanism] + [makeSortMechanism, t] ); const tableRows: RowDef[] = useMemo( @@ -296,6 +323,7 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( [allData] ); + const [cellGroupEventEmitter] = useState(() => new EventEmitter()); const tableData = useMemo( () => allData.map((poolWithMetrics) => { @@ -306,11 +334,9 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( coinDenom: poolAsset.amount.currency.coinDenom, }) ); - const isIncentivized = - queryOsmosis.queryIncentivizedPools.isIncentivized(poolId); return [ - { poolId, poolAssets, isIncentivized }, + { poolId, poolAssets }, { value: poolWithMetrics.liquidity.toString() }, { value: poolWithMetrics.apr?.toString(), @@ -318,15 +344,29 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( }, { value: poolWithMetrics.epochsRemaining?.toString() }, { value: poolWithMetrics.myLiquidity?.toString() }, + { + poolId, + cellGroupEventEmitter, + onAddLiquidity: () => quickAddLiquidity(poolId), + onRemoveLiquidity: !poolWithMetrics.myAvailableLiquidity + .toDec() + .isZero() + ? () => quickRemoveLiquidity(poolId) + : undefined, + onLockTokens: !poolWithMetrics.myAvailableLiquidity + .toDec() + .isZero() + ? () => quickLockTokens(poolId) + : undefined, + }, ]; }), - [allData, queryOsmosis] + [allData, queryOsmosis.queryIncentivizedPools.isAprFetching] ); if (isMobile) { return ( ({ id: poolData.pool.id, assets: poolData.pool.poolAssets.map( @@ -353,9 +393,12 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( ], ...[ sortKeyPath === "apr" - ? { label: "TVL", value: poolData.liquidity.toString() } + ? { + label: t("pools.externalIncentivized.TVL"), + value: poolData.liquidity.toString(), + } : { - label: "APR", + label: t("pools.externalIncentivized.APR"), value: poolData.apr?.toString() ?? "0%", }, ], @@ -367,7 +410,7 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( searchBoxProps={{ currentValue: query, onInput: setQuery, - placeholder: "Filtery by symbol", + placeholder: t("pools.externalIncentivized.search"), }} sortMenuProps={{ options: tableCols, @@ -389,12 +432,12 @@ export const ExternalIncentivizedPoolsTableSet: FunctionComponent = observer( return ( <>
-
External Incentive Pools
-
+
{t("pools.externalIncentivized.title")}
+
= observer( @@ -12,73 +13,69 @@ export const StepBase: FunctionComponent<{ step: 1 | 2 | 3 } & StepProps> = step, createPoolConfig: config, isSendingMsg, - backStep, advanceStep, children, }) => { const { isMobile } = useWindowSize(); + const t = useTranslation(); + const positiveBalanceError = t( + config.positiveBalanceError?.message ?? "" + ); + const percentageError = t(config.percentageError?.message ?? ""); + const amountError = t(config.amountError?.message ?? ""); + const swapFeeError = t(config.swapFeeError?.message ?? ""); - const positiveBalanceError = config.positiveBalanceError?.message; - const percentageError = config.percentageError?.message; - const amountError = config.amountError?.message; - const swapFeeError = config.swapFeeError?.message; const canAdvance = (step === 1 && !percentageError && !positiveBalanceError) || (step === 2 && !amountError) || (step === 3 && config.acknowledgeFee && !swapFeeError); + const currentErrorMessage = + step === 1 + ? percentageError || positiveBalanceError + : step === 2 + ? amountError + : swapFeeError; + return (
- - Step {step} / 3 - + {step === 1 - ? " Set token ratios" + ? t("pools.createPool.step.one", { + step: step.toString(), + nbStep: "3", + }) : step === 2 - ? " Input amount to add" + ? t("pools.createPool.step.two", { + step: step.toString(), + nbStep: "3", + }) : step === 3 - ? " Confirm pool ratio and token amount" + ? t("pools.createPool.step.three", { + step: step.toString(), + nbStep: "3", + }) : null}{" "} -
{children}
- {positiveBalanceError && step === 1 && ( - - )} - {!positiveBalanceError && percentageError && step === 1 && ( - + {step === 1 && ( + )} - {amountError && step === 2 && ( - - )} - {swapFeeError && step === 3 && ( - - )} -
- {step !== 1 && ( - - )} - -
+
); } diff --git a/packages/web/components/complex/pool/create/step1-set-ratios.tsx b/packages/web/components/complex/pool/create/step1-set-ratios.tsx index e7552ead8e..e9029b218d 100644 --- a/packages/web/components/complex/pool/create/step1-set-ratios.tsx +++ b/packages/web/components/complex/pool/create/step1-set-ratios.tsx @@ -7,12 +7,14 @@ import { InputBox } from "../../../input"; import { StepProps } from "./types"; import { StepBase } from "./step-base"; import { useWindowSize } from "../../../../hooks"; -import { Button } from "../../../buttons"; +import { BorderButton } from "../../../buttons"; +import { useTranslation } from "react-multi-lang"; export const Step1SetRatios: FunctionComponent = observer( (props) => { const { createPoolConfig: config } = props; const { isMobile } = useWindowSize(); + const t = useTranslation(); return ( @@ -20,50 +22,53 @@ export const Step1SetRatios: FunctionComponent = observer( {config.assets.map(({ amountConfig, percentage }, index) => (
{ const currency = config.remainingSelectableCurrencies.find( (currency) => currency.coinDenom === coinDenom ); if (currency) { amountConfig.setSendCurrency(currency); + } else { + console.error( + "Unable to find currency selected in TokenSelect to be set in create pool config" + ); } }} - isMobile={isMobile} />
- + config.setAssetPercentageAt(index, value)} placeholder="" + trailingSymbol="%" + rightEntry /> - %
))}
diff --git a/packages/web/components/complex/pool/create/step2-add-liquidity.tsx b/packages/web/components/complex/pool/create/step2-add-liquidity.tsx index 88b43da3cf..3701112451 100644 --- a/packages/web/components/complex/pool/create/step2-add-liquidity.tsx +++ b/packages/web/components/complex/pool/create/step2-add-liquidity.tsx @@ -2,15 +2,16 @@ import Image from "next/image"; import { FunctionComponent } from "react"; import { observer } from "mobx-react-lite"; import { InputBox } from "../../../input"; -import { Button } from "../../../buttons"; import { StepBase } from "./step-base"; import { StepProps } from "./types"; import { useWindowSize } from "../../../../hooks"; +import { useTranslation } from "react-multi-lang"; export const Step2AddLiquidity: FunctionComponent = observer( (props) => { const { createPoolConfig: config } = props; const { isMobile } = useWindowSize(); + const t = useTranslation(); return ( @@ -27,49 +28,44 @@ export const Step2AddLiquidity: FunctionComponent = observer( key={amountConfig.sendCurrency.coinDenom} className="h-24 md:h-fit flex px-7 md:p-2 items-center place-content-between border border-white-faint rounded-2xl" > -
-
- {currency.coinImageUrl && ( -
- token icon -
- )} -
+
+ {currency.coinImageUrl && ( +
+ token icon +
+ )}
{isMobile ? ( {justCoinDenom} ) : (
{justCoinDenom}
)} -
+
{percentage}%
-
-
- - Balance:{" "} +
+
+ + {t("pools.createPool.available")} + + amountConfig.setIsMax(true)} + > {config.queryBalances .getQueryBech32Address(config.sender) .getBalanceFromCurrency(amountConfig.sendCurrency) .maxDecimals(6) .toString()} -
= observer( onInput={(value) => amountConfig.setAmount(value)} placeholder="" /> - {!isMobile &&
{justCoinDenom}
}
diff --git a/packages/web/components/complex/pool/create/step3-confirm.tsx b/packages/web/components/complex/pool/create/step3-confirm.tsx index 204afeddcf..9a8cf6a223 100644 --- a/packages/web/components/complex/pool/create/step3-confirm.tsx +++ b/packages/web/components/complex/pool/create/step3-confirm.tsx @@ -1,6 +1,5 @@ import { FunctionComponent, useMemo } from "react"; import { observer } from "mobx-react-lite"; -import { runInAction } from "mobx"; import { InputBox } from "../../../input"; import { PieChart, @@ -13,10 +12,12 @@ import { IBCCurrency } from "@keplr-wallet/types"; import { CheckBox } from "../../../control"; import { POOL_CREATION_FEE } from "."; import { useWindowSize } from "../../../../hooks"; +import { useTranslation } from "react-multi-lang"; export const Step3Confirm: FunctionComponent = observer((props) => { const { createPoolConfig: config } = props; const { isMobile } = useWindowSize(); + const t = useTranslation(); const series = useMemo(() => { return generateSeries( @@ -31,7 +32,7 @@ export const Step3Confirm: FunctionComponent = observer((props) => { return (
-
+
= observer((props) => { />
-
- Token - Amount +
+ {t("pools.createPool.token")} + {t("pools.createPool.amount")}
{config.assets.map( ( @@ -85,7 +86,7 @@ export const Step3Confirm: FunctionComponent = observer((props) => {
{"paths" in sendCurrency ? ( - + {(sendCurrency as IBCCurrency).paths .map((path) => path.channelId) .join(", ")} @@ -93,7 +94,7 @@ export const Step3Confirm: FunctionComponent = observer((props) => { ) : (
)} - + {percentage}%
@@ -103,8 +104,8 @@ export const Step3Confirm: FunctionComponent = observer((props) => { )}
-
- Set Swap Fee +
+ {t("pools.createPool.swapFee")}
= observer((props) => { currentValue={config.swapFee} onInput={(value) => config.setSwapFee(value)} placeholder="" + trailingSymbol="%" /> - {isMobile ? % :
%
}
-
- { - runInAction(() => { - config.acknowledgeFee = !config.acknowledgeFee; - }); - }} - > - {isMobile ? ( -
- I understand that creating a new pool will cost{" "} - {POOL_CREATION_FEE}. -
- ) : ( - `I understand that creating a new pool will cost ${POOL_CREATION_FEE}.` - )} -
+
+
+ (config.acknowledgeFee = !config.acknowledgeFee)} + > + {isMobile ? ( +
+ {t("pools.createPool.undersandCost", { POOL_CREATION_FEE })} +
+ ) : ( + t("pools.createPool.undersandCost", { POOL_CREATION_FEE }) + )} +
+
diff --git a/packages/web/components/complex/pool/create/types.ts b/packages/web/components/complex/pool/create/types.ts index 4683c89a38..89601f446c 100644 --- a/packages/web/components/complex/pool/create/types.ts +++ b/packages/web/components/complex/pool/create/types.ts @@ -4,6 +4,5 @@ import { CustomClasses } from "../../../types"; export interface StepProps extends CustomClasses { createPoolConfig: ObservableCreatePoolConfig; isSendingMsg?: boolean; - backStep: () => void; advanceStep: () => void; } diff --git a/packages/web/components/complex/remove-liquidity.tsx b/packages/web/components/complex/remove-liquidity.tsx new file mode 100644 index 0000000000..52669f0fba --- /dev/null +++ b/packages/web/components/complex/remove-liquidity.tsx @@ -0,0 +1,111 @@ +import Image from "next/image"; +import { FunctionComponent, ReactNode } from "react"; +import classNames from "classnames"; +import { observer } from "mobx-react-lite"; +import { useTranslation } from "react-multi-lang"; +import { Dec } from "@keplr-wallet/unit"; +import { ObservableRemoveLiquidityConfig } from "@osmosis-labs/stores"; +import { Slider } from "../../components/control"; +import { BorderButton } from "../buttons"; +import { CustomClasses } from "../types"; +import { useStore } from "../../stores"; + +export const RemoveLiquidity: FunctionComponent< + { + removeLiquidityConfig: ObservableRemoveLiquidityConfig; + actionButton: ReactNode; + } & CustomClasses +> = observer(({ className, removeLiquidityConfig, actionButton }) => { + const { priceStore } = useStore(); + const t = useTranslation(); + + return ( + <> +
+
+

{`${removeLiquidityConfig.computePoolShareValueWithPercentage( + priceStore + )}`}

+
+ {t("removeLiquidity.sharesAmount", { + shares: removeLiquidityConfig.poolShareWithPercentage + .trim(true) + .hideDenom(true) + .toString(), + })} +
+
+
+ {removeLiquidityConfig.poolShareAssetsWithPercentage.map((asset) => ( +
+ {asset.currency.coinImageUrl && ( + token icon + )} + + {asset.trim(true).toString()} + +
+ ))} +
+ + removeLiquidityConfig.setPercentage(value.toString()) + } + min={0} + max={100} + step={1} + /> +
+ removeLiquidityConfig.setPercentage("25")} + disabled={removeLiquidityConfig.poolShareWithPercentage + .toDec() + .equals(new Dec(0))} + > + 25% + + removeLiquidityConfig.setPercentage("50")} + disabled={removeLiquidityConfig.poolShareWithPercentage + .toDec() + .equals(new Dec(0))} + > + 50% + + removeLiquidityConfig.setPercentage("75")} + disabled={removeLiquidityConfig.poolShareWithPercentage + .toDec() + .equals(new Dec(0))} + > + 75% + + removeLiquidityConfig.setPercentage("100")} + disabled={removeLiquidityConfig.poolShareWithPercentage + .toDec() + .equals(new Dec(0))} + > + {t("components.MAX")} + +
+
+ {actionButton} + + ); +}); diff --git a/packages/web/components/complex/sidebar-bottom.tsx b/packages/web/components/complex/sidebar-bottom.tsx deleted file mode 100644 index 92ef2ba875..0000000000 --- a/packages/web/components/complex/sidebar-bottom.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import Image from "next/image"; -import { FunctionComponent } from "react"; -import { observer } from "mobx-react-lite"; -import { WalletStatus } from "@keplr-wallet/stores"; -import { PricePretty, Dec } from "@keplr-wallet/unit"; -import { useAmplitudeAnalytics } from "../../hooks"; -import { useStore } from "../../stores"; -import { EventName, IS_FRONTIER } from "../../config"; - -export const SidebarBottom: FunctionComponent = observer(() => { - const { chainStore, accountStore, queriesStore, priceStore } = useStore(); - const { logEvent, setUserProperty } = useAmplitudeAnalytics(); - - const account = accountStore.getAccount(chainStore.osmosis.chainId); - const queries = queriesStore.get(chainStore.osmosis.chainId); - const fiat = priceStore.getFiatCurrency(priceStore.defaultVsCurrency); - const osmoPrice = fiat - ? new PricePretty( - fiat, - new Dec( - priceStore.getPrice( - chainStore.osmosis.stakeCurrency.coinGeckoId ?? "osmosis" - ) ?? 0 - ) - ) - : undefined; - - return ( -
e.stopPropagation()}> - {osmoPrice && ( -
-
-
- osmo -
- - {chainStore.osmosis.stakeCurrency.coinDenom.toUpperCase()} - -
- {osmoPrice.toString()} -
- )} -
- {account.walletStatus === WalletStatus.Loaded ? ( -
-
-
- wallet -
-
-

- {account.name} -

-

- {queries.queryBalances - .getQueryBech32Address(account.bech32Address) - .stakable.balance.trim(true) - .maxDecimals(2) - .shrink(true) - .upperCase(true) - .toString()} -

-
-
- -
- ) : ( - - )} -
- {IS_FRONTIER ? ( -

- - Learn More
About the
- - Osmosis Frontier - -
-

- link -
-

- ) : ( - <> - - - - )} -
- ); -}); diff --git a/packages/web/components/complex/transfer.tsx b/packages/web/components/complex/transfer.tsx index 71a74125c1..42e1ec971d 100644 --- a/packages/web/components/complex/transfer.tsx +++ b/packages/web/components/complex/transfer.tsx @@ -9,9 +9,10 @@ import { BridgeAnimation } from "../animation/bridge"; import { SwitchWalletButton } from "../buttons/switch-wallet"; import { GradientView } from "../assets/gradient-view"; import { InputBox } from "../input"; -import { Button } from "../buttons"; +import { BorderButton } from "../buttons"; import { CheckBox } from "../control"; import { Disableable, InputProps, LoadingProps } from "../types"; +import { useTranslation } from "react-multi-lang"; export type TransferProps = { isWithdraw: boolean; @@ -59,6 +60,7 @@ export const Transfer: FunctionComponent = ({ disablePanel = false, }) => { const { isMobile } = useWindowSize(); + const t = useTranslation(); const [isEditingWithdrawAddr, setIsEditingWithdrawAddr] = useState(false); @@ -75,10 +77,10 @@ export const Transfer: FunctionComponent = ({ const panelDisabled = disablePanel || bridge?.isLoading || false; const maxFromChars = isEditingWithdrawAddr - ? 12 // can't be on mobile + ? 13 // can't be on mobile : !from.address.startsWith("osmo") && selectedWalletDisplay ? isMobile - ? 10 + ? 13 : 18 // more space for switch wallet button : isMobile ? 14 @@ -87,7 +89,7 @@ export const Transfer: FunctionComponent = ({ (!to.address.startsWith("osmo") && selectedWalletDisplay) || // make room for btns editWithdrawAddrConfig ? isMobile - ? 10 + ? 13 : 18 : isMobile ? 14 @@ -101,16 +103,16 @@ export const Transfer: FunctionComponent = ({ />
@@ -120,7 +122,7 @@ export const Transfer: FunctionComponent = ({ isOsmosisAccountLoaded ? ( Bech32Address.shortenAddress(from.address, maxFromChars) ) : ( - Connect Wallet + {t("connectWallet")} ) ) : ( truncateEthAddress(from.address) @@ -140,7 +142,7 @@ export const Transfer: FunctionComponent = ({
= ({ maxToChars ) ) : ( - Connect Wallet + {t("connectWallet")} ) ) : ( truncateEthAddress(to.address) @@ -177,16 +179,15 @@ export const Transfer: FunctionComponent = ({ editWithdrawAddrConfig && !panelDisabled && !isEditingWithdrawAddr && ( - + {t("assets.ibcTransfer.buttonEdit")} + )} {isEditingWithdrawAddr && editWithdrawAddrConfig && ( = ({ }} labelButtons={[ { - label: "Enter", + label: t("assets.ibcTransfer.buttonEnter"), className: - "bg-primary-50 hover:bg-primary-50 border-0 rounded-md", + "bg-wosmongton-100 hover:bg-wosmongton-100 border-0 rounded-md", onClick: () => setIsEditingWithdrawAddr(false), disabled: !editWithdrawAddrConfig.isValid, }, @@ -221,9 +222,11 @@ export const Transfer: FunctionComponent = ({
{isMobile ? ( - Select Amount + + {t("assets.ibcTransfer.selectAmount")} + ) : ( -
Select Amount
+
{t("assets.ibcTransfer.selectAmount")}
)}
= ({ : "opacity-0" )} > - Available{!isMobile && ` on ${from.networkName}`}:{" "} + {isMobile + ? t("assets.transfer.availableMobile") + : t("assets.transfer.availableOn", { + network: from.networkName, + })}{" "} + {isMobile ? ( + setDropdownOpen(false)} + options={options} + title={title} + /> + ) : ( + + )} +
+ ); + } +); diff --git a/packages/web/components/control/icon-dropdown/index.ts b/packages/web/components/control/icon-dropdown/index.ts new file mode 100644 index 0000000000..b4418b7cdd --- /dev/null +++ b/packages/web/components/control/icon-dropdown/index.ts @@ -0,0 +1,3 @@ +export * from "./menu-dropdown-icon"; +export * from "./icon-dropdown"; +export * from "./menu-dropdown-icon-item"; diff --git a/packages/web/components/control/icon-dropdown/menu-dropdown-icon-item.tsx b/packages/web/components/control/icon-dropdown/menu-dropdown-icon-item.tsx new file mode 100644 index 0000000000..ebef00c974 --- /dev/null +++ b/packages/web/components/control/icon-dropdown/menu-dropdown-icon-item.tsx @@ -0,0 +1,51 @@ +import { FunctionComponent } from "react"; +import Image from "next/image"; +import classNames from "classnames"; +import React from "react"; +import { useTranslation } from "react-multi-lang"; +import { MenuDropdownIconItemProps } from "../types"; + +interface Props { + option: MenuDropdownIconItemProps; + currentOption: MenuDropdownIconItemProps; + index: number; + optionLength: number; + onSelect: onSelectIconDropdown; +} + +export type onSelectIconDropdown = (option: MenuDropdownIconItemProps) => void; + +export const MenuDropdownIconItem: FunctionComponent = ({ + option, + onSelect, + index, + optionLength, +}: Props) => { + const t = useTranslation(); + + return ( + + ); +}; diff --git a/packages/web/components/control/icon-dropdown/menu-dropdown-icon.tsx b/packages/web/components/control/icon-dropdown/menu-dropdown-icon.tsx new file mode 100644 index 0000000000..46cb8ad4c1 --- /dev/null +++ b/packages/web/components/control/icon-dropdown/menu-dropdown-icon.tsx @@ -0,0 +1,44 @@ +import React, { FunctionComponent } from "react"; +import classNames from "classnames"; +import { MenuDropdownIconItemProps } from "../types"; +import { + MenuDropdownIconItem, + onSelectIconDropdown, +} from "./menu-dropdown-icon-item"; + +interface Props { + currentOption: MenuDropdownIconItemProps; + isOpen: boolean; + options: MenuDropdownIconItemProps[]; + onSelect: onSelectIconDropdown; +} +export const MenuDropdownIcon: FunctionComponent = ({ + onSelect, + currentOption, + options, + isOpen, +}: Props) => { + return ( +
+ {options.map((option: MenuDropdownIconItemProps, index: number) => { + return ( + + ); + })} +
+ ); +}; diff --git a/packages/web/components/control/index.ts b/packages/web/components/control/index.ts index b4bb71e38f..6587ee1963 100644 --- a/packages/web/components/control/index.ts +++ b/packages/web/components/control/index.ts @@ -9,4 +9,7 @@ export * from "./switch"; export * from "./tab-box"; export * from "./toggle"; export * from "./token-select"; +export * from "./language-select"; +export * from "./icon-dropdown"; +export * from "./icon-dropdown/icon-dropdown"; export * from "./types"; diff --git a/packages/web/components/control/language-select.tsx b/packages/web/components/control/language-select.tsx new file mode 100644 index 0000000000..4f2ae8099b --- /dev/null +++ b/packages/web/components/control/language-select.tsx @@ -0,0 +1,40 @@ +import React, { FunctionComponent } from "react"; +import { observer } from "mobx-react-lite"; +import { useStore } from "../../stores"; +import { MenuDropdownIconItemProps } from "./types"; +import { useTranslation } from "react-multi-lang"; +import { IconDropdown } from "./icon-dropdown/icon-dropdown"; +import { LanguageUserSetting } from "../../stores/user-settings"; + +export type LanguageSelectProps = { + options: MenuDropdownIconItemProps[]; +}; + +export const LanguageSelect: FunctionComponent = observer( + ({ options }: LanguageSelectProps) => { + const { userSettings } = useStore(); + const t = useTranslation(); + const languageSetting = userSettings.getUserSettingById( + "language" + ) as LanguageUserSetting; + const currentLanguage = languageSetting?.state.language; + const currentOption = options.find( + (option) => option.value === currentLanguage + ); + + const onSelect = (option: MenuDropdownIconItemProps) => { + userSettings + .getUserSettingById("language") + ?.setState({ language: option.value }); + }; + + return ( + option.value !== currentLanguage)} + currentOption={currentOption ?? languageSetting.defaultLanguage} + title={t("settings.titleLanguage")} + /> + ); + } +); diff --git a/packages/web/components/control/menu-dropdown.tsx b/packages/web/components/control/menu-dropdown.tsx index d287b49f52..74d68d0810 100644 --- a/packages/web/components/control/menu-dropdown.tsx +++ b/packages/web/components/control/menu-dropdown.tsx @@ -5,10 +5,7 @@ import { CustomClasses } from "../types"; interface Props extends MenuSelectProps, CustomClasses { isOpen: boolean; - /** Default: `"left"` */ - openDropdownHDirection?: "left" | "right"; - /** Default: `"down"` */ - openDropdownVDirection?: "down" | "up"; + isFloating?: boolean; } /** @@ -19,18 +16,15 @@ export const MenuDropdown: FunctionComponent = ({ selectedOptionId, onSelect, isOpen, - openDropdownHDirection = "left", - openDropdownVDirection = "down", + isFloating = false, className, }) => (
= ({ {options.map(({ id, display }, index) => (
{isMobile ? ( = ({ /> ) : ( )}
diff --git a/packages/web/components/control/switch.tsx b/packages/web/components/control/switch.tsx index 38c613f080..974a414d09 100644 --- a/packages/web/components/control/switch.tsx +++ b/packages/web/components/control/switch.tsx @@ -4,39 +4,64 @@ import { Disableable, CustomClasses } from "../types"; import { ToggleProps } from "./types"; export const Switch: FunctionComponent< - ToggleProps & Disableable & CustomClasses & { containerClassName?: string } + ToggleProps & + Disableable & + CustomClasses & { + containerClassName?: string; + labelPosition?: "left" | "right"; + } > = ({ isOn, onToggle, disabled = false, containerClassName, className, + labelPosition = "left", children, }) => ( ); diff --git a/packages/web/components/control/tab-box.tsx b/packages/web/components/control/tab-box.tsx index 8daca6c870..9162d97412 100644 --- a/packages/web/components/control/tab-box.tsx +++ b/packages/web/components/control/tab-box.tsx @@ -48,11 +48,6 @@ export const TabBox: FunctionComponent< key={index} className={classNames( "w-full text-center py-1 px-5 cursor-pointer", - { - "border-b-2 border-secondary-200": selectedTabI === index, - "border-b opacity-40 border-white-full/[.12] hover:opacity-60": - selectedTabI !== index, - }, tabClassName )} onClick={() => { @@ -61,7 +56,13 @@ export const TabBox: FunctionComponent< }} > {typeof title === "string" ? ( - + {title} ) : ( diff --git a/packages/web/components/control/toggle.tsx b/packages/web/components/control/toggle.tsx index 04dc42c3ed..29fd87e276 100644 --- a/packages/web/components/control/toggle.tsx +++ b/packages/web/components/control/toggle.tsx @@ -19,12 +19,12 @@ export const Toggle: FunctionComponent< className={classNames( "absolute h-6 w-full rounded-lg appearance-none", { - "opacity-30 bg-iconDefault": disabled && isOn, - "opacity-10 bg-iconDefault": disabled && !isOn, - "bg-primary-200": !disabled && isOn, + "opacity-30 bg-osmoverse-400": disabled && isOn, + "opacity-10 bg-osmoverse-400": disabled && !isOn, + "bg-wosmongton-200": !disabled && isOn, "cursor-pointer": isHovered && !disabled, - "bg-primary-100": isHovered && !disabled && isOn, - "bg-surface": !disabled && !isOn, + "bg-wosmongton-100": isHovered && !disabled && isOn, + "bg-osmoverse-900": !disabled && !isOn, }, className )} diff --git a/packages/web/components/control/token-select.tsx b/packages/web/components/control/token-select.tsx index 805c997036..9d9dbbeefc 100644 --- a/packages/web/components/control/token-select.tsx +++ b/packages/web/components/control/token-select.tsx @@ -1,35 +1,36 @@ import Image from "next/image"; import { FunctionComponent, useEffect, useRef } from "react"; import { observer } from "mobx-react-lite"; -import { AppCurrency, IBCCurrency } from "@keplr-wallet/types"; +import { AppCurrency } from "@keplr-wallet/types"; import { CoinPretty } from "@keplr-wallet/unit"; import { useStore } from "../../stores"; import { TokenSelectModal } from "../../modals"; -import { useBooleanWithWindowEvent, useFilteredData } from "../../hooks"; -import { MobileProps } from "../types"; +import { + useBooleanWithWindowEvent, + useFilteredData, + useWindowSize, +} from "../../hooks"; import classNames from "classnames"; /** Will display balances if provided `CoinPretty` objects. Assumes denoms are unique. */ -export const TokenSelect: FunctionComponent< - { - selectedTokenDenom: string; - tokens: (CoinPretty | AppCurrency)[]; - onSelect: (tokenDenom: string) => void; - sortByBalances?: boolean; - dropdownOpen?: boolean; - setDropdownState?: (isOpen: boolean) => void; - } & MobileProps -> = observer( +export const TokenSelect: FunctionComponent<{ + selectedTokenDenom: string; + tokens: (CoinPretty | AppCurrency)[]; + onSelect: (tokenDenom: string) => void; + sortByBalances?: boolean; + dropdownOpen?: boolean; + setDropdownState?: (isOpen: boolean) => void; +}> = observer( ({ selectedTokenDenom, tokens, onSelect, sortByBalances = false, - isMobile = false, dropdownOpen, setDropdownState, }) => { const { chainStore, priceStore } = useStore(); + const { isMobile } = useWindowSize(); // parent overrideable state const [isSelectOpenLocal, setIsSelectOpenLocal] = @@ -129,9 +130,10 @@ export const TokenSelect: FunctionComponent<
{selectedCurrency && (
)} -
-
+
+
{isMobile ? ( {selectedDenom} ) : ( @@ -170,7 +172,7 @@ export const TokenSelect: FunctionComponent<
)}
-
+
{chainStore.getChainFromCurrency(selectedCurrency.coinDenom) ?.chainName ?? ""}
@@ -178,123 +180,17 @@ export const TokenSelect: FunctionComponent< )} - {isMobile ? ( - { - setTokenSearch(""); - setIsSelectOpen(false); - }} - currentValue={searchValue} - onInput={(v) => setTokenSearch(v)} - tokens={searchedTokens} - onSelect={onSelect} - /> - ) : ( - isSelectOpen && ( -
e.stopPropagation()} - > -
-
- search -
- e.stopPropagation()} - value={searchValue} - onInput={(e: any) => setTokenSearch(e.target.value)} - /> -
- -
    - {searchedTokens.map((t, index) => { - const currency = - t.token instanceof CoinPretty ? t.token.currency : t.token; - const { coinDenom, coinImageUrl } = currency; - const networkName = t.chainName; - const justDenom = - coinDenom.split(" ").slice(0, 1).join(" ") ?? ""; - const channel = - "paths" in currency - ? (currency as IBCCurrency).paths[0].channelId - : undefined; - - const showChannel = coinDenom.includes("channel"); - const fiatValue = - t.token instanceof CoinPretty && !t.token.toDec().isZero() - ? priceStore.calculatePrice(t.token)?.toString() - : undefined; - - return ( -
  • { - e.stopPropagation(); - onSelect(coinDenom); - setTokenSearch(""); - setIsSelectOpen(false); - }} - > - -
  • - ); - })} -
-
- ) - )} + { + setTokenSearch(""); + setIsSelectOpen(false); + }} + currentValue={searchValue} + onInput={(v) => setTokenSearch(v)} + tokens={searchedTokens} + onSelect={onSelect} + />
); } diff --git a/packages/web/components/control/types.ts b/packages/web/components/control/types.ts index f8f6886253..c50da464b1 100644 --- a/packages/web/components/control/types.ts +++ b/packages/web/components/control/types.ts @@ -20,3 +20,9 @@ export interface NumberSelectProps extends InputProps { min: number; max: number; } + +export interface MenuDropdownIconItemProps { + value: string; + display: string; + iconUrl?: string; +} diff --git a/packages/web/components/input/input-box.tsx b/packages/web/components/input/input-box.tsx index f77f71be9c..9b164fff4c 100644 --- a/packages/web/components/input/input-box.tsx +++ b/packages/web/components/input/input-box.tsx @@ -22,6 +22,8 @@ interface Props extends InputProps, Disableable, CustomClasses { labelButtons?: Button[]; /** Show a clear button when `currentValue !== ""`. */ clearButton?: boolean; + /** Display a symbol after the input box, ex: '%'. */ + trailingSymbol?: string; inputClassName?: string; isAutosize?: boolean; inputRef?: React.MutableRefObject; @@ -37,6 +39,7 @@ export const InputBox: FunctionComponent = ({ rightEntry = false, labelButtons = [], clearButton = false, + trailingSymbol, inputClassName, disabled = false, className, @@ -48,20 +51,23 @@ export const InputBox: FunctionComponent = ({ return (
-
+
+
+ { + // allow global event to close dropdown when clicking settings button + if (!settingsDropdownOpen) setSettingsDropdownOpen(true); + }} + /> + {settingsDropdownOpen && ( + + )} +
+ +
+
+ {/* Back-layer element to occupy space for the caller */} +
+ + ); +}); + +const NavBarButton: FunctionComponent< + { + iconurl: string; + hovericonurl: string; + } & ButtonHTMLAttributes +> = (props) => { + const { iconurl, hovericonurl } = props; + const [hovered, setHovered] = useState(false); + + return ( + + ); +}; + +const SettingsDropdown: FunctionComponent<{ + userSettings: IUserSetting[]; +}> = observer(({ userSettings }) => { + const t = useTranslation(); + return ( +
e.stopPropagation()} + > +
{t("settings.title")}
+
+ {userSettings.map((setting) => ( +
+ + {setting.getLabel(t)} + + {setting.controlComponent(setting.state as any, setting.setState)} +
+ ))} +
+
+ ); +}); + +const WalletInfo: FunctionComponent = observer( + ({ className }) => { + const { + chainStore: { + osmosis: { chainId }, + }, + accountStore, + navBarStore, + } = useStore(); + const t = useTranslation(); + const { isMobile } = useWindowSize(); + + // wallet + const account = accountStore.getAccount(chainId); + const walletConnected = account.walletStatus === WalletStatus.Loaded; + const [hoverWalletInfo, setHoverWalletInfo] = useState(false); + + // mobile: show disconnect on tap vs hover + const [mobileTapInfo, setMobileTapInfo] = useState(false); + + return ( +
+ {!walletConnected ? ( + + ) : hoverWalletInfo || mobileTapInfo ? ( + + ) : ( +
{ + if (!isMobile) setHoverWalletInfo(true); + }} + onClick={() => { + if (isMobile) setMobileTapInfo(true); + }} + > +
+ wallet-icon +
+ +
+ + {navBarStore.walletInfo.balance.toString()} + + + {navBarStore.walletInfo.name} + +
+
+ )} +
+ ); + } +); diff --git a/packages/web/components/ogp-meta.tsx b/packages/web/components/ogp-meta.tsx index 5cf4492f6d..f88c608342 100644 --- a/packages/web/components/ogp-meta.tsx +++ b/packages/web/components/ogp-meta.tsx @@ -9,33 +9,14 @@ import { IS_FRONTIER } from "../config"; * Picks a random preview image amongst a selection */ export const OgpMeta: FunctionComponent = () => { - const origin = - typeof window === "undefined" - ? IS_FRONTIER - ? "https://frontier.osmosis.zone" - : "https://app.osmosis.zone" - : window.origin; - const previewText = IS_FRONTIER ? "The Osmosis Frontier" : "Trade on Osmosis Zone"; - const previewImages = IS_FRONTIER - ? [origin + "/images/osmosis-cowboy-woz.png"] - : [ - origin + "/images/osmosis-home-bg-low.png", - origin + "/images/osmosis-liquidity-lab.png", - ]; - return ( - + ); }; diff --git a/packages/web/components/overview/index.tsx b/packages/web/components/overview/index.tsx deleted file mode 100644 index a90f83a012..0000000000 --- a/packages/web/components/overview/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { ComponentProps, FunctionComponent, ReactElement } from "react"; -import classNames from "classnames"; -import { useWindowSize } from "../../hooks"; -import { OverviewLabelValue } from "./overview-label-value"; -import { Button } from "../buttons/button"; -import { CustomClasses, Metric } from "../types"; - -interface LabelButton extends ComponentProps, CustomClasses { - label: string; -} - -interface Props { - /** Title at top left of overview. */ - title: string | ReactElement; - /** Label buttons to the right of the title at the top. - * Accepts at most 2. - */ - titleButtons?: LabelButton[]; - /** First row of overview labels, with more prominent value text size. - * Accepts at most 2. 4 if there is no background image. - */ - primaryOverviewLabels: Metric[]; - /** Second row of overview labels, with slightly less prominent value text size. - * Accepts at most 3. - */ - secondaryOverviewLabels?: Metric[]; - /** - * Image url of the right-fixed background image. - */ - bgImageUrl?: string; -} - -export const Overview: FunctionComponent = ({ - title, - titleButtons, - primaryOverviewLabels, - secondaryOverviewLabels, - bgImageUrl, -}) => { - const { width, isMobile } = useWindowSize(); - - return ( -
-
-
-
- {typeof title === "string" ? ( - isMobile ? ( -
{title}
- ) : ( -
{title}
- ) - ) : ( - <>{title} - )} -
- {titleButtons?.slice(0, 2).map((props, index) => ( - - ))} -
-
-
- {primaryOverviewLabels - .slice(0, bgImageUrl ? 2 : 4) - .map((label, index) => ( - - ))} -
- {secondaryOverviewLabels && ( -
- {secondaryOverviewLabels.slice(0, 3).map((label, index) => ( - - ))} -
- )} -
-
-
- ); -}; diff --git a/packages/web/components/overview/overview-label-value.tsx b/packages/web/components/overview/overview-label-value.tsx deleted file mode 100644 index e2b89fc2da..0000000000 --- a/packages/web/components/overview/overview-label-value.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { FunctionComponent } from "react"; -import classNames from "classnames"; -import { CustomClasses, Metric, MobileProps } from "../types"; - -interface Props extends Metric, MobileProps { - containerClassName?: string; - labelClassName?: string; - valueClassName?: string; - prominence?: "primary" | "secondary"; -} - -export const OverviewLabelValue: FunctionComponent = ({ - containerClassName, - labelClassName, - valueClassName, - prominence = "primary", - label, - value, - isMobile = false, -}) => ( -
- - {label} - - {prominence === "primary" ? ( - - {value} - - ) : ( - - {value} - - )} -
-); - -export const PrimaryMetric: FunctionComponent = ({ - isMobile = false, - className, - children, -}) => - isMobile ? ( -
{children}
- ) : ( -

{children}

- ); - -export const SecondaryMetric: FunctionComponent< - MobileProps & CustomClasses -> = ({ isMobile = false, className, children }) => - isMobile ? ( - {children} - ) : ( -
{children}
- ); diff --git a/packages/web/components/overview/pools.tsx b/packages/web/components/overview/pools.tsx new file mode 100644 index 0000000000..0279f5135e --- /dev/null +++ b/packages/web/components/overview/pools.tsx @@ -0,0 +1,109 @@ +import Image from "next/image"; +import { FunctionComponent, useState, useEffect } from "react"; +import classNames from "classnames"; +import dayjs from "dayjs"; +import { CoinPretty, DecUtils } from "@keplr-wallet/unit"; +import { useStore } from "../../stores"; +import { useWindowSize } from "../../hooks"; +import { CustomClasses, Breakpoint } from "../types"; +import { useTranslation } from "react-multi-lang"; + +const REWARD_EPOCH_IDENTIFIER = "day"; + +export const PoolsOverview: FunctionComponent<{} & CustomClasses> = ({ + className, +}) => { + const { chainStore, priceStore, queriesStore } = useStore(); + const { width } = useWindowSize(); + + const { chainId } = chainStore.osmosis; + const queryOsmosis = queriesStore.get(chainId).osmosis!; + const t = useTranslation(); + + const osmoPrice = priceStore.calculatePrice( + new CoinPretty( + chainStore.osmosis.stakeCurrency, + DecUtils.getTenExponentNInPrecisionRange( + chainStore.osmosis.stakeCurrency.coinDecimals + ) + ) + ); + + // update time every second + const [timeRemaining, setTimeRemaining] = useState(null); + useEffect(() => { + const updateTimeRemaining = () => { + const queryEpoch = queryOsmosis.queryEpochs.getEpoch( + REWARD_EPOCH_IDENTIFIER + ); + const now = new Date(); + const epochRemainingTime = dayjs.duration( + dayjs(queryEpoch.endTime).diff(dayjs(now), "second"), + "second" + ); + const epochRemainingTimeString = + epochRemainingTime.asSeconds() <= 0 + ? dayjs.duration(0, "seconds").format("HH-mm-ss") + : epochRemainingTime.format("HH-mm-ss"); + const [epochRemainingHour, epochRemainingMinute, epochRemainingSeconds] = + epochRemainingTimeString.split("-"); + setTimeRemaining( + `${epochRemainingHour}:${epochRemainingMinute}:${epochRemainingSeconds}` + ); + }; + const intervalId = setInterval(updateTimeRemaining, 1000); + updateTimeRemaining(); + + return () => clearInterval(intervalId); + }, []); + + return ( +
+
+
+ {t("pools.priceOsmo")} +
+

+ {osmoPrice?.toString()} +

+
+
+
+ {t("pools.rewardDistribution")} +
+

+ {timeRemaining} +

+
+
+ lab machine +
+
+ ); +}; diff --git a/packages/web/components/pixels/pallete.tsx b/packages/web/components/pixels/pallete.tsx index e3bae8d596..4303d016fe 100644 --- a/packages/web/components/pixels/pallete.tsx +++ b/packages/web/components/pixels/pallete.tsx @@ -41,7 +41,7 @@ const Palette = ({ }} >
{ openShareModal(); diff --git a/packages/web/components/table/assets-table.tsx b/packages/web/components/table/assets-table.tsx index c5e92100c8..884d813842 100644 --- a/packages/web/components/table/assets-table.tsx +++ b/packages/web/components/table/assets-table.tsx @@ -1,6 +1,7 @@ +import Image from "next/image"; import { FunctionComponent, useCallback, useMemo, useState } from "react"; import { Dec } from "@keplr-wallet/unit"; -import { initialAssetsSort } from "../../config"; +import { BUY_OSMO_TRANSAK, initialAssetsSort } from "../../config"; import { IBCBalance, IBCCW20ContractBalance, @@ -12,13 +13,12 @@ import { useLocalStorageState, useWindowSize, useAmplitudeAnalytics, + useShowDustUserSetting, } from "../../hooks"; import { ShowMoreButton } from "../buttons/show-more"; import { SearchBox } from "../input"; import { SortMenu, Switch } from "../control"; import { SortDirection } from "../types"; -import { AssetCard } from "../cards"; -import { Button } from "../buttons"; import { AssetNameCell, BalanceCell, @@ -29,6 +29,8 @@ import { TransferHistoryTable } from "./transfer-history"; import { ColumnDef } from "./types"; import { Table } from "."; import { EventName } from "../../config/user-analytics-v2"; +import { observer } from "mobx-react-lite"; +import { useTranslation } from "react-multi-lang"; interface Props { nativeBalances: CoinBalance[]; @@ -37,103 +39,72 @@ interface Props { withdrawUrlOverride?: string; sourceChainNameOverride?: string; })[]; - onWithdrawIntent: () => void; - onDepositIntent: () => void; onWithdraw: ( chainId: string, coinDenom: string, externalUrl?: string ) => void; onDeposit: (chainId: string, coinDenom: string, externalUrl?: string) => void; + onBuyOsmo: () => void; } -export const AssetsTable: FunctionComponent = ({ - nativeBalances, - ibcBalances, - onDepositIntent, - onWithdrawIntent, - onDeposit: do_onDeposit, - onWithdraw: do_onWithdraw, -}) => { - const { chainStore } = useStore(); - const { width, isMobile } = useWindowSize(); - const { logEvent } = useAmplitudeAnalytics(); +export const AssetsTable: FunctionComponent = observer( + ({ + nativeBalances, + ibcBalances, + onDeposit: do_onDeposit, + onWithdraw: do_onWithdraw, + onBuyOsmo, + }) => { + const { chainStore } = useStore(); + const { width, isMobile } = useWindowSize(); + const t = useTranslation(); + const { logEvent } = useAmplitudeAnalytics(); - const onDeposit = useCallback( - (...depositParams: Parameters) => { - do_onDeposit(...depositParams); - logEvent([ - EventName.Assets.assetsItemDepositClicked, - { - tokenName: depositParams[1], - hasExternalUrl: !!depositParams[2], - }, - ]); - }, - [do_onDeposit] - ); - const onWithdraw = useCallback( - (...withdrawParams: Parameters) => { - do_onWithdraw(...withdrawParams); - logEvent([ - EventName.Assets.assetsItemWithdrawClicked, - { - tokenName: withdrawParams[1], - hasExternalUrl: !!withdrawParams[2], - }, - ]); - }, - [do_onWithdraw] - ); + const onDeposit = useCallback( + (...depositParams: Parameters) => { + do_onDeposit(...depositParams); + logEvent([ + EventName.Assets.assetsItemDepositClicked, + { + tokenName: depositParams[1], + hasExternalUrl: !!depositParams[2], + }, + ]); + }, + [do_onDeposit, logEvent] + ); + const onWithdraw = useCallback( + (...withdrawParams: Parameters) => { + do_onWithdraw(...withdrawParams); + logEvent([ + EventName.Assets.assetsItemWithdrawClicked, + { + tokenName: withdrawParams[1], + hasExternalUrl: !!withdrawParams[2], + }, + ]); + }, + [do_onWithdraw, logEvent] + ); - const mergeWithdrawCol = width < 1000 && !isMobile; - // Assemble cells with all data needed for any place in the table. - const cells: TableCell[] = useMemo( - () => [ - // hardcode native Osmosis assets (OSMO, ION) at the top initially - ...nativeBalances.map(({ balance, fiatValue }) => { - const value = fiatValue?.maxDecimals(2); + const dustIbcBalances = useShowDustUserSetting(ibcBalances, (ibcBalance) => + !ibcBalance.balance.toDec().isZero() ? ibcBalance.fiatValue : undefined + ); - return { - value: balance.toString(), - currency: balance.currency, - chainId: chainStore.osmosis.chainId, - chainName: "", - coinDenom: balance.denom, - coinImageUrl: balance.currency.coinImageUrl, - amount: balance.hideDenom(true).trim(true).maxDecimals(6).toString(), - fiatValue: - value && value.toDec().gt(new Dec(0)) - ? value.toString() - : undefined, - fiatValueRaw: - value && value.toDec().gt(new Dec(0)) - ? value?.toDec().toString() - : "0", - isCW20: false, - }; - }), - ...initialAssetsSort( - ibcBalances.map((ibcBalance) => { - const { - chainInfo: { chainId, chainName }, - balance, - fiatValue, - depositUrlOverride, - withdrawUrlOverride, - sourceChainNameOverride, - } = ibcBalance; + const mergeWithdrawCol = width < 1000 && !isMobile; + // Assemble cells with all data needed for any place in the table. + const cells: TableCell[] = useMemo( + () => [ + // hardcode native Osmosis assets (OSMO, ION) at the top initially + ...nativeBalances.map(({ balance, fiatValue }) => { const value = fiatValue?.maxDecimals(2); - const isCW20 = "ics20ContractAddress" in ibcBalance; - const pegMechanism = balance.currency.originCurrency?.pegMechanism; return { value: balance.toString(), currency: balance.currency, - chainName: sourceChainNameOverride - ? sourceChainNameOverride - : chainName, - chainId: chainId, + chainId: chainStore.osmosis.chainId, + chainName: "", coinDenom: balance.denom, coinImageUrl: balance.currency.coinImageUrl, amount: balance @@ -149,160 +120,179 @@ export const AssetsTable: FunctionComponent = ({ value && value.toDec().gt(new Dec(0)) ? value?.toDec().toString() : "0", - queryTags: [ - ...(isCW20 ? ["CW20"] : []), - ...(pegMechanism ? ["stable", pegMechanism] : []), - ], - isUnstable: ibcBalance.isUnstable === true, - depositUrlOverride, - withdrawUrlOverride, - onWithdraw, - onDeposit, + isCW20: false, + onBuyOsmo: + balance.denom === "OSMO" && BUY_OSMO_TRANSAK + ? onBuyOsmo + : undefined, }; - }) - ), - ], - [ - nativeBalances, - chainStore.osmosis.chainId, - ibcBalances, - onWithdraw, - onDeposit, - ] - ); + }), + ...initialAssetsSort( + dustIbcBalances.map((ibcBalance) => { + const { + chainInfo: { chainId, chainName }, + balance, + fiatValue, + depositUrlOverride, + withdrawUrlOverride, + sourceChainNameOverride, + } = ibcBalance; + const value = fiatValue?.maxDecimals(2); + const isCW20 = "ics20ContractAddress" in ibcBalance; + const pegMechanism = balance.currency.originCurrency?.pegMechanism; + + return { + value: balance.toString(), + currency: balance.currency, + chainName: sourceChainNameOverride + ? sourceChainNameOverride + : chainName, + chainId: chainId, + coinDenom: balance.denom, + coinImageUrl: balance.currency.coinImageUrl, + amount: balance + .hideDenom(true) + .trim(true) + .maxDecimals(6) + .toString(), + fiatValue: + value && value.toDec().gt(new Dec(0)) + ? value.toString() + : undefined, + fiatValueRaw: + value && value.toDec().gt(new Dec(0)) + ? value?.toDec().toString() + : "0", + queryTags: [ + ...(isCW20 ? ["CW20"] : []), + ...(pegMechanism ? ["stable", pegMechanism] : []), + ], + isUnstable: ibcBalance.isUnstable === true, + depositUrlOverride, + withdrawUrlOverride, + onWithdraw, + onDeposit, + }; + }) + ), + ], + [nativeBalances, chainStore.osmosis.chainId, dustIbcBalances] + ); - // Sort data based on user's input either with the table column headers or the sort menu. - const [ - sortKey, - do_setSortKey, - sortDirection, - setSortDirection, - toggleSortDirection, - sortedCells, - ] = useSortedData(cells); - const setSortKey = useCallback( - (term: string) => { - logEvent([ - EventName.Assets.assetsListSorted, - { - sortedBy: term, - sortDirection, + // Sort data based on user's input either with the table column headers or the sort menu. + const [ + sortKey, + do_setSortKey, + sortDirection, + setSortDirection, + toggleSortDirection, + sortedCells, + ] = useSortedData(cells); + const setSortKey = useCallback( + (term: string) => { + logEvent([ + EventName.Assets.assetsListSorted, + { + sortedBy: term, + sortDirection, - sortedOn: "dropdown", - }, - ]); - do_setSortKey(term); - }, - [sortDirection, do_setSortKey] - ); + sortedOn: "dropdown", + }, + ]); + do_setSortKey(term); + }, + [sortDirection] + ); - // Table column def to determine how the first 2 column headers handle user click. - const sortColumnWithKeys = useCallback( - ( - /** Possible cell keys/members this column can sort on. First key is default - * sort key if this column header is selected. - */ - sortKeys: string[], - /** Default sort direction when this column is first selected. */ - onClickSortDirection: SortDirection = "descending" - ) => { - const isSorting = sortKeys.some((key) => key === sortKey); - const firstKey = sortKeys.find((_, i) => i === 0); + // Table column def to determine how the first 2 column headers handle user click. + const sortColumnWithKeys = useCallback( + ( + /** Possible cell keys/members this column can sort on. First key is default + * sort key if this column header is selected. + */ + sortKeys: string[], + /** Default sort direction when this column is first selected. */ + onClickSortDirection: SortDirection = "descending" + ) => { + const isSorting = sortKeys.some((key) => key === sortKey); + const firstKey = sortKeys.find((_, i) => i === 0); - return { - currentDirection: isSorting ? sortDirection : undefined, - // Columns can sort by more than one key. If the column is already sorting by - // one of it's sort keys (one that the user may have selected from the sort menu), - // then it will toggle sort direction on that key. - // If it wasn't sorting (aka first time it is clicked), then it will sort on the first - // key by default. - onClickHeader: isSorting - ? () => { - logEvent([ - EventName.Assets.assetsListSorted, - { - sortedBy: firstKey, - sortDirection: - sortDirection === "descending" ? "ascending" : "descending", - sortedOn: "table-head", - }, - ]); - toggleSortDirection(); - } - : () => { - if (firstKey) { + return { + currentDirection: isSorting ? sortDirection : undefined, + // Columns can sort by more than one key. If the column is already sorting by + // one of it's sort keys (one that the user may have selected from the sort menu), + // then it will toggle sort direction on that key. + // If it wasn't sorting (aka first time it is clicked), then it will sort on the first + // key by default. + onClickHeader: isSorting + ? () => { logEvent([ EventName.Assets.assetsListSorted, { sortedBy: firstKey, - sortDirection: onClickSortDirection, + sortDirection: + sortDirection === "descending" + ? "ascending" + : "descending", sortedOn: "table-head", }, ]); - setSortKey(firstKey); - setSortDirection(onClickSortDirection); + toggleSortDirection(); } - }, - }; - }, - [sortKey, sortDirection, toggleSortDirection, setSortKey, setSortDirection] - ); + : () => { + if (firstKey) { + logEvent([ + EventName.Assets.assetsListSorted, + { + sortedBy: firstKey, + sortDirection: onClickSortDirection, + sortedOn: "table-head", + }, + ]); + setSortKey(firstKey); + setSortDirection(onClickSortDirection); + } + }, + }; + }, + [sortKey, sortDirection] + ); - // User toggles for showing 10+ pools and assets with > 0 fiat value - const [showAllAssets, setShowAllAssets] = useState(false); - const [hideZeroBalances, setHideZeroBalances] = useLocalStorageState( - "assets_hide_zero_balances", - false - ); - const canHideZeroBalances = cells.some((cell) => cell.amount !== "0"); + // User toggles for showing 10+ pools and assets with > 0 fiat value + const [showAllAssets, setShowAllAssets] = useState(false); + const [hideZeroBalances, setHideZeroBalances] = useLocalStorageState( + "assets_hide_zero_balances", + false + ); + const canHideZeroBalances = cells.some((cell) => cell.amount !== "0"); - // Filter data based on user's input in the search box. - const [query, setQuery, filteredSortedCells] = useFilteredData( - hideZeroBalances - ? sortedCells.filter((cell) => cell.amount !== "0") - : sortedCells, - ["chainName", "chainId", "coinDenom", "amount", "fiatValue", "queryTags"] - ); + // Filter data based on user's input in the search box. + const [query, setQuery, filteredSortedCells] = useFilteredData( + hideZeroBalances + ? sortedCells.filter((cell) => cell.amount !== "0") + : sortedCells, + ["chainName", "chainId", "coinDenom", "amount", "fiatValue", "queryTags"] + ); - const tableData = showAllAssets - ? filteredSortedCells - : filteredSortedCells.slice(0, 10); + const tableData = showAllAssets + ? filteredSortedCells + : filteredSortedCells.slice(0, 10); - return ( -
-
+ return ( +
{isMobile ? (
-
- - -
+
{t("assets.table.title")}
{ setHideZeroBalances(false); setQuery(query); }} - placeholder="Filter by symbol" + placeholder={t("assets.table.search")} /> -
Assets
-
+
= ({ setHideZeroBalances(!hideZeroBalances); }} > - Hide zero balances + + {t("assets.table.hideZero")} + = ({ options={[ { id: "coinDenom", - display: "Symbol", + display: t("assets.table.sort.symbol"), }, { /** These ids correspond to keys in `Cell` type and are later used for sorting. */ id: "chainName", - display: "Network", + display: t("assets.table.sort.network"), }, { - id: "amount", - display: "Balance", + id: "fiatValueRaw", + display: t("assets.table.sort.balance"), }, ]} /> @@ -344,25 +336,25 @@ export const AssetsTable: FunctionComponent = ({
) : (
-
Assets
-
- { - setHideZeroBalances(!hideZeroBalances); - }} - > - Hide zero balances - -
+
+
{t("assets.table.title")}
+
+ { + setHideZeroBalances(!hideZeroBalances); + }} + > + {t("assets.table.hideZero")} + { setHideZeroBalances(false); setQuery(query); }} - placeholder="Search assets" + placeholder={t("assets.table.search")} /> = ({ options={[ { id: "coinDenom", - display: "Symbol", + display: t("assets.table.sort.symbol"), }, { /** These ids correspond to keys in `Cell` type and are later used for sorting. */ id: "chainName", - display: "Network", + display: t("assets.table.sort.network"), }, { id: "fiatValueRaw", - display: "Balance", + display: t("assets.table.sort.balance"), }, ]} /> @@ -404,16 +396,9 @@ export const AssetsTable: FunctionComponent = ({ {isMobile ? (
{tableData.map((assetData) => ( - = ({ } } } - showArrow - /> + > +
+ {assetData.coinImageUrl && ( +
+ token icon +
+ )} +
+
{assetData.coinDenom}
+ {assetData.chainName && ( + + {assetData.chainName} + + )} +
+
+
+
+
+ {assetData.amount} +
+ {assetData.fiatValue && ( + {assetData.fiatValue} + )} +
+ {!( + assetData.chainId === undefined || + (assetData.chainId && + assetData.chainId === chainStore.osmosis.chainId) + ) && ( + select asset + )} +
+
))}
) : ( @@ -434,12 +461,12 @@ export const AssetsTable: FunctionComponent = ({ className="w-full my-5" columnDefs={[ { - display: "Asset / Chain", + display: t("assets.table.columns.assetChain"), displayCell: AssetNameCell, sort: sortColumnWithKeys(["coinDenom", "chainName"]), }, { - display: "Balance", + display: t("assets.table.columns.balance"), displayCell: BalanceCell, sort: sortColumnWithKeys(["fiatValueRaw"], "descending"), className: "text-right pr-24 lg:pr-8 1.5md:pr-1", @@ -447,30 +474,30 @@ export const AssetsTable: FunctionComponent = ({ ...(mergeWithdrawCol ? ([ { - display: "Transfer", + display: t("assets.table.columns.transfer"), displayCell: (cell) => (
), - className: "text-center max-w-[5rem]", + className: "text-left max-w-[5rem]", }, ] as ColumnDef[]) : ([ { - display: "Deposit", + display: t("assets.table.columns.deposit"), displayCell: (cell) => ( ), - className: "text-center max-w-[5rem]", + className: "text-left max-w-[5rem]", }, { - display: "Withdraw", + display: t("assets.table.columns.withdraw"), displayCell: (cell) => ( ), - className: "text-center max-w-[5rem]", + className: "text-left max-w-[5rem]", }, ] as ColumnDef[])), ]} @@ -500,7 +527,7 @@ export const AssetsTable: FunctionComponent = ({ )}
-
-
- ); -}; +
+ ); + } +); diff --git a/packages/web/components/table/cells/asset-name.tsx b/packages/web/components/table/cells/asset-name.tsx index 5968cf7a94..f1e3f9c34b 100644 --- a/packages/web/components/table/cells/asset-name.tsx +++ b/packages/web/components/table/cells/asset-name.tsx @@ -22,12 +22,10 @@ export const AssetNameCell: FunctionComponent> = ({ {coinDenom}
{chainName && ( - {chainName} + {chainName} )}
- {isUnstable && ( - - )} + {isUnstable && }
) : ( {coinDenom} diff --git a/packages/web/components/table/cells/balance.tsx b/packages/web/components/table/cells/balance.tsx index bd6af4c7d1..9a46cc60e3 100644 --- a/packages/web/components/table/cells/balance.tsx +++ b/packages/web/components/table/cells/balance.tsx @@ -8,6 +8,8 @@ export const BalanceCell: FunctionComponent> = ({ amount ? (
{amount} - {fiatValue && {fiatValue}} + {fiatValue && ( + {fiatValue} + )}
) : null; diff --git a/packages/web/components/table/cells/index.ts b/packages/web/components/table/cells/index.ts index 68b6974907..4a1db0607b 100644 --- a/packages/web/components/table/cells/index.ts +++ b/packages/web/components/table/cells/index.ts @@ -2,6 +2,7 @@ export * from "./asset-name"; export * from "./balance"; export * from "./transfer-button"; export * from "./pool-composition"; +export * from "./pool-quick-actions"; export * from "./metric-loader-cell"; export * from "./types"; export * from "./validator-info"; diff --git a/packages/web/components/table/cells/pool-composition.tsx b/packages/web/components/table/cells/pool-composition.tsx index ac1249ad7f..65773e3b69 100644 --- a/packages/web/components/table/cells/pool-composition.tsx +++ b/packages/web/components/table/cells/pool-composition.tsx @@ -1,6 +1,6 @@ import classNames from "classnames"; -import Image from "next/image"; import React, { FunctionComponent } from "react"; +import { useTranslation } from "react-multi-lang"; import { BaseCell } from ".."; import { PoolAssetsIcon, PoolAssetsName } from "../../assets"; @@ -10,7 +10,6 @@ export interface PoolCompositionCell extends BaseCell { coinImageUrl: string | undefined; coinDenom: string; }[]; - isIncentivized?: boolean; } /** Displays pool composition as a cell in a table. @@ -19,27 +18,20 @@ export interface PoolCompositionCell extends BaseCell { */ export const PoolCompositionCell: FunctionComponent< Partial -> = ({ poolId, poolAssets, isIncentivized = false }) => ( -
- -
- asset.coinDenom)} - /> - - Pool #{poolId} - -
- {isIncentivized && ( -
- trade = ({ poolId, poolAssets }) => { + const t = useTranslation(); + return ( +
+ +
+ asset.coinDenom)} /> + + {t("components.table.poolId", { id: poolId ? poolId : "-" })} +
- )} -
-); +
+ ); +}; diff --git a/packages/web/components/table/cells/pool-quick-actions.tsx b/packages/web/components/table/cells/pool-quick-actions.tsx new file mode 100644 index 0000000000..324f0eaeb1 --- /dev/null +++ b/packages/web/components/table/cells/pool-quick-actions.tsx @@ -0,0 +1,126 @@ +import Image from "next/image"; +import React, { + FunctionComponent, + useCallback, + useEffect, + useMemo, +} from "react"; +import EventEmitter from "eventemitter3"; +import { useBooleanWithWindowEvent } from "../../../hooks"; +import { MenuDropdown, MenuOption } from "../../control"; +import { BaseCell } from ".."; +import { PoolCompositionCell } from "./pool-composition"; +import { useTranslation } from "react-multi-lang"; + +export interface PoolQuickActionCell + extends BaseCell, + Pick { + /** Used to group quick action cells, to close dropdowns via events aren't related to this cell. */ + cellGroupEventEmitter?: EventEmitter; + onAddLiquidity?: () => void; + onRemoveLiquidity?: () => void; + onLockTokens?: () => void; +} + +/** Displays pool composition as a cell in a table. + * + * Accepts the base hover flag. + */ +export const PoolQuickActionCell: FunctionComponent< + Partial +> = ({ + poolId, + cellGroupEventEmitter, + onAddLiquidity, + onRemoveLiquidity, + onLockTokens, +}) => { + const [dropdownOpen, setDropdownOpen] = useBooleanWithWindowEvent(false); + const t = useTranslation(); + + const menuOptions = useMemo(() => { + const m: MenuOption[] = []; + + if (onAddLiquidity) { + m.push({ + id: "add-liquidity", + display: t("addLiquidity.title"), + }); + } + if (onRemoveLiquidity) { + m.push({ + id: "remove-liquidity", + display: t("removeLiquidity.title"), + }); + } + if (onLockTokens) { + m.push({ + id: "lock-tokens", + display: t("lockToken.title"), + }); + } + + return m; + }, [onAddLiquidity, onRemoveLiquidity, onLockTokens, t]); + + const doAction = useCallback( + (optionId) => { + setDropdownOpen(false); + + switch (optionId) { + case "add-liquidity": + onAddLiquidity?.(); + break; + case "remove-liquidity": + onRemoveLiquidity?.(); + break; + case "lock-tokens": + onLockTokens?.(); + break; + } + }, + [poolId, onAddLiquidity, onRemoveLiquidity, onLockTokens, setDropdownOpen] + ); + + useEffect(() => { + if (cellGroupEventEmitter) { + const onPoolSelected = (selectedPoolId: string) => { + if (selectedPoolId !== poolId) { + setDropdownOpen(false); + } + }; + cellGroupEventEmitter.on("select-pool-id", onPoolSelected); + + return () => { + cellGroupEventEmitter.removeListener("select-pool-id", onPoolSelected); + }; + } + }, [poolId]); + + return ( +
{ + e.stopPropagation(); + setDropdownOpen(!dropdownOpen); + cellGroupEventEmitter?.emit("select-pool-id", poolId); + }} + > +
{ + e.preventDefault(); + }} + > + menu + doAction(id)} + isFloating + /> +
+
+ ); +}; diff --git a/packages/web/components/table/cells/transfer-button.tsx b/packages/web/components/table/cells/transfer-button.tsx index b46bcc2570..c88171766d 100644 --- a/packages/web/components/table/cells/transfer-button.tsx +++ b/packages/web/components/table/cells/transfer-button.tsx @@ -1,8 +1,10 @@ import Image from "next/image"; import classNames from "classnames"; -import { FunctionComponent } from "react"; -import { Button } from "../../../components/buttons/button"; +import { FunctionComponent, useState } from "react"; +import { WalletStatus } from "@keplr-wallet/stores"; import { AssetCell as Cell } from "./types"; +import { useStore } from "../../../stores"; +import { useTranslation } from "react-multi-lang"; export const TransferButtonCell: FunctionComponent< { @@ -19,35 +21,52 @@ export const TransferButtonCell: FunctionComponent< isUnstable, onWithdraw, onDeposit, -}) => - type === "withdraw" ? ( + onBuyOsmo, +}) => { + const t = useTranslation(); + const { chainStore, accountStore } = useStore(); + + const account = accountStore.getAccount(chainStore.osmosis.chainId); + + return type === "withdraw" ? ( chainId && coinDenom && onWithdraw ? ( onWithdraw?.(chainId, coinDenom, withdrawUrlOverride)} /> ) : null - ) : chainId && coinDenom && onDeposit ? ( + ) : chainId && coinDenom && (onDeposit || onBuyOsmo) ? ( onDeposit?.(chainId, coinDenom, depositUrlOverride)} + label={ + onBuyOsmo ? t("assets.table.buyOsmo") : t("assets.table.depositButton") + } + action={ + onBuyOsmo + ? onBuyOsmo + : () => onDeposit?.(chainId, coinDenom, depositUrlOverride) + } /> ) : null; +}; const TransferButton: FunctionComponent<{ externalUrl?: string; disabled?: boolean; label: string; action: () => void; -}> = ({ externalUrl, disabled, label, action }) => - externalUrl ? ( +}> = ({ externalUrl, disabled, label, action }) => { + const [isHovering, setIsHovering] = useState(false); + return externalUrl ? ( {label} - external transfer link +
+ external transfer link +
) : ( - + {isHovering ? ( + chevron + ) : ( + chevron + )} + ); +}; diff --git a/packages/web/components/table/cells/types.ts b/packages/web/components/table/cells/types.ts index 0cac3ff72e..68a2697656 100644 --- a/packages/web/components/table/cells/types.ts +++ b/packages/web/components/table/cells/types.ts @@ -24,6 +24,7 @@ export type AssetCell = BaseCell & { coinDenom: string, externalUrl?: string ) => void; + onBuyOsmo?: () => void; }; export interface ValidatorInfo extends BaseCell { diff --git a/packages/web/components/table/cells/validator-info.tsx b/packages/web/components/table/cells/validator-info.tsx index 20ce8b15fb..eaa6bc6fc4 100644 --- a/packages/web/components/table/cells/validator-info.tsx +++ b/packages/web/components/table/cells/validator-info.tsx @@ -6,7 +6,7 @@ export const ValidatorInfoCell: FunctionComponent = ({ imgSrc, }) => (
-
+
= observer(({ poolId, tableClassName, className }) => { const { chainStore, accountStore, queriesStore } = useStore(); + const t = useTranslation(); const { chainId } = chainStore.osmosis; const { isMobile } = useWindowSize(); @@ -53,22 +55,17 @@ export const DepoolingTable: FunctionComponent< return (
{isMobile ? ( - Depoolings + {t("pool.depoolings.titleMobile")} ) : ( -
Depoolings
- )} - {poolId && ( - +
{t("pool.depoolings.title")}
)} + {poolId && } [ { diff --git a/packages/web/components/table/index.tsx b/packages/web/components/table/index.tsx index dc6748a581..8ac4a45f9b 100644 --- a/packages/web/components/table/index.tsx +++ b/packages/web/components/table/index.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import Link from "next/link"; +import { useRouter } from "next/router"; import React, { PropsWithoutRef, useState, @@ -43,9 +44,10 @@ export const Table = ({ tBodyClassName, }: PropsWithoutRef>) => { const { width } = useWindowSize(); + const router = useRouter(); + // pass row hovered to cell components. Tailwind preferred for tr/tds. const [rowsHovered, setRowsHovered] = useState(() => data.map(() => false)); - const setRowHovered = useCallback( (rowIndex: number, value: boolean) => setRowsHovered( @@ -61,7 +63,7 @@ export const Table = ({ return (
- + {columnDefs.map((colDef, colIndex) => { if (colDef.collapseAt && width < colDef.collapseAt) { return null; @@ -81,10 +83,12 @@ export const Table = ({ - +
{colDef?.display ? ( typeof colDef.display === "string" ? ( - colDef.display + + {colDef.display} + ) : ( <>{colDef.display} ) @@ -119,7 +123,7 @@ export const Table = ({ {colDef.infoTooltip && ( )} - +
); @@ -138,24 +142,27 @@ export const Table = ({
setRowHovered(rowIndex, true)} onMouseLeave={() => setRowHovered(rowIndex, false)} onClick={() => { - if (rowDef !== undefined) { - rowDef.onClick?.(rowIndex); - } + if (rowDef?.link) { + router.push(rowDef.link); + } else rowDef?.onClick?.(rowIndex); }} > + {/* layout row's cells */} {row.map((cell, columnIndex) => { const DisplayCell = columnDefs[columnIndex]?.displayCell; const customClass = columnDefs[columnIndex]?.className; @@ -166,7 +173,20 @@ export const Table = ({ } return ( -
+ {rowDef?.link ? ( diff --git a/packages/web/components/table/transfer-history.tsx b/packages/web/components/table/transfer-history.tsx index b80f48c96c..6d4e48b92a 100644 --- a/packages/web/components/table/transfer-history.tsx +++ b/packages/web/components/table/transfer-history.tsx @@ -13,6 +13,7 @@ import { Table, BaseCell } from "."; import { Breakpoint, CustomClasses } from "../types"; import { truncateString } from "../utils"; import { useWindowSize } from "../../hooks"; +import { useTranslation } from "react-multi-lang"; type History = { txHash: string; @@ -32,7 +33,7 @@ export const TransferHistoryTable: FunctionComponent = observer( ibcTransferHistoryStore, accountStore, } = useStore(); - + const t = useTranslation(); const { chainId } = chainStore.osmosis; const { bech32Address } = accountStore.getAccount(chainId); @@ -93,7 +94,7 @@ export const TransferHistoryTable: FunctionComponent = observer( return histories.length > 0 ? ( <>
- Transfer History + {t("assets.historyTable.title")}
className={classNames("w-full", className)} @@ -101,14 +102,14 @@ export const TransferHistoryTable: FunctionComponent = observer( tBodyClassName="body2 md:caption" columnDefs={[ { - display: "Transaction Hash", + display: t("assets.historyTable.colums.transactionHash"), className: "md:!pl-2", displayCell: TxHashDisplayCell, }, - { display: "Type" }, - { display: "Amount" }, + { display: t("assets.historyTable.colums.type") }, + { display: t("assets.historyTable.colums.amount") }, { - display: "Status", + display: t("assets.historyTable.colums.status"), collapseAt: Breakpoint.SM, className: "md:!pr-2", displayCell: StatusDisplayCell, @@ -118,7 +119,9 @@ export const TransferHistoryTable: FunctionComponent = observer( { ...history, value: history.txHash }, // Tx Hash { // Type - value: history.isWithdraw ? "Withdraw" : "Deposit", + value: history.isWithdraw + ? t("assets.historyTable.colums.deposit") + : t("assets.historyTable.colums.withdraw"), }, { // Amount @@ -160,6 +163,7 @@ const TxHashDisplayCell: FunctionComponent< const StatusDisplayCell: FunctionComponent< BaseCell & { status?: IBCTransferHistoryStatus | "failed"; reason?: string } > = ({ status, reason }) => { + const t = useTranslation(); if (status == null) { // Uncommitted history has no status. // Show pending for uncommitted history.. @@ -173,7 +177,7 @@ const StatusDisplayCell: FunctionComponent< height={24} /> - Pending + {t("assets.historyTable.pending")} ); } @@ -188,7 +192,7 @@ const StatusDisplayCell: FunctionComponent< width={24} height={24} /> - Success + {t("assets.historyTable.success")} ); case "pending": @@ -202,14 +206,14 @@ const StatusDisplayCell: FunctionComponent< height={24} /> - Pending + {t("assets.historyTable.pending")} ); case "refunded": return (
failed - Refunded + {t("assets.historyTable.refunded")}
); case "timeout": @@ -223,7 +227,9 @@ const StatusDisplayCell: FunctionComponent< height={24} /> - Failed: Pending refund + + {t("assets.historyTable.pendingRefunded")} + ); case "failed": diff --git a/packages/web/components/table/types.ts b/packages/web/components/table/types.ts index febc4279b4..c118cd460d 100644 --- a/packages/web/components/table/types.ts +++ b/packages/web/components/table/types.ts @@ -27,7 +27,7 @@ export interface ColumnDef extends CustomClasses { * Note: components must accept optionals for all cell data and check for the data they need. */ displayCell?: React.FunctionComponent>; /** Use to make your table responsive. Uses `use-window-size/Breakpoint` to incrementally - * remove whole columns from display as the screen shrinks from `XXL` to `MD` size. + * remove whole columns from display as the screen shrinks in order from `XXL` to `MD` size. */ collapseAt?: Breakpoint; } diff --git a/packages/web/components/tooltip/info.tsx b/packages/web/components/tooltip/info.tsx index 40b2089282..7f0e03c239 100644 --- a/packages/web/components/tooltip/info.tsx +++ b/packages/web/components/tooltip/info.tsx @@ -10,20 +10,12 @@ const Tippy = dynamic(() => import("@tippyjs/react"), { ssr: false }); export const InfoTooltip: FunctionComponent< TooltipProps & CustomClasses & { - style?: "iconDefault" | "secondary-200"; size?: { height: number; width: number }; iconSrcOverride?: string; } -> = ({ - content, - trigger, - style = "iconDefault", - size, - iconSrcOverride, - className, -}) => ( +> = ({ content, trigger, size, iconSrcOverride, className }) => ( @@ -33,13 +25,7 @@ export const InfoTooltip: FunctionComponent< > info { + if (account.walletStatus !== WalletStatus.Loaded) { + return account.init(); + } + if (tradeTokenInConfig.optimizedRoutePaths.length > 0) { + const routes: { + poolId: string; + tokenOutCurrency: Currency; + }[] = []; + + for ( + let i = 0; + i < tradeTokenInConfig.optimizedRoutePaths[0].pools.length; + i++ + ) { + const pool = tradeTokenInConfig.optimizedRoutePaths[0].pools[i]; + const tokenOutCurrency = chainStore.osmosisObservable.currencies.find( + (cur) => + cur.coinMinimalDenom === + tradeTokenInConfig.optimizedRoutePaths[0].tokenOutDenoms[i] + ); + + if (!tokenOutCurrency) { + tradeTokenInConfig.setError( + new Error( + t("swap.error.findCurrency", { + currency: + tradeTokenInConfig.optimizedRoutePaths[0].tokenOutDenoms[i], + }) + ) + ); + return; + } + + routes.push({ + poolId: pool.id, + tokenOutCurrency, + }); + } + + const tokenInCurrency = chainStore.osmosisObservable.currencies.find( + (cur) => + cur.coinMinimalDenom === + tradeTokenInConfig.optimizedRoutePaths[0].tokenInDenom + ); + + if (!tokenInCurrency) { + tradeTokenInConfig.setError( + new Error( + t("swap.error.findCurrency", { + currency: tradeTokenInConfig.optimizedRoutePaths[0].tokenInDenom, + }) + ) + ); + return; + } + + const tokenIn = { + currency: tokenInCurrency, + amount: tradeTokenInConfig.amount, + }; + const maxSlippage = slippageConfig.slippage.symbol("").toString(); + + try { + logEvent([ + EventName.Swap.swapStarted, + { + fromToken: tradeTokenInConfig.sendCurrency.coinDenom, + tokenAmount: Number(tokenIn.amount), + toToken: tradeTokenInConfig.outCurrency.coinDenom, + isOnHome: !isInModal, + isMultiHop: routes.length !== 1, + }, + ]); + if (routes.length === 1) { + await account.osmosis.sendSwapExactAmountInMsg( + routes[0].poolId, + tokenIn, + routes[0].tokenOutCurrency, + maxSlippage, + "", + { + amount: [ + { + denom: chainStore.osmosis.stakeCurrency.coinMinimalDenom, + amount: "0", + }, + ], + }, + { + preferNoSetFee: preferZeroFee, + }, + () => { + logEvent([ + EventName.Swap.swapCompleted, + { + fromToken: tradeTokenInConfig.sendCurrency.coinDenom, + tokenAmount: Number(tokenIn.amount), + toToken: tradeTokenInConfig.outCurrency.coinDenom, + isOnHome: !isInModal, + + isMultiHop: false, + }, + ]); + } + ); + } else { + await account.osmosis.sendMultihopSwapExactAmountInMsg( + routes, + tokenIn, + maxSlippage, + "", + { + amount: [ + { + denom: chainStore.osmosis.stakeCurrency.coinMinimalDenom, + amount: "0", + }, + ], + }, + { + preferNoSetFee: preferZeroFee, + }, + () => { + logEvent([ + EventName.Swap.swapCompleted, + { + fromToken: tradeTokenInConfig.sendCurrency.coinDenom, + tokenAmount: Number(tokenIn.amount), + toToken: tradeTokenInConfig.outCurrency.coinDenom, + isOnHome: !isInModal, + isMultiHop: true, + }, + ]); + } + ); + } + tradeTokenInConfig.setAmount(""); + tradeTokenInConfig.setFraction(undefined); + } catch (e) { + console.error(e); + } + } + }; + return (
-
Swap
+
{t("swap.title")}
{isSettingOpen && (
e.stopPropagation()} > -
- Transaction Settings -
+
{t("swap.settings.title")}
-
- Slippage tolerance +
+ {t("swap.settings.slippage")}
- +
    @@ -279,8 +424,8 @@ export const TradeClipboard: FunctionComponent<{
  • { e.preventDefault(); @@ -301,15 +446,15 @@ export const TradeClipboard: FunctionComponent<{ })}
  • { e.preventDefault(); @@ -324,7 +469,7 @@ export const TradeClipboard: FunctionComponent<{ className="bg-transparent px-0 w-fit" inputClassName={`bg-transparent text-center ${ !slippageConfig.isManualSlippage - ? "text-white-faint" + ? "text-osmoverse-500" : "text-white-high" }`} style="no-border" @@ -346,7 +491,13 @@ export const TradeClipboard: FunctionComponent<{ inputRef={manualSlippageInputRef} isAutosize /> - % + + % +
@@ -356,7 +507,7 @@ export const TradeClipboard: FunctionComponent<{
- Available + {t("swap.available")} - + {queries.queryBalances .getQueryBech32Address(account.bech32Address) .getBalanceFromCurrency(tradeTokenInConfig.sendCurrency) @@ -393,16 +544,14 @@ export const TradeClipboard: FunctionComponent<{
- - + {t("swap.HALF")} +
@@ -487,7 +634,6 @@ export const TradeClipboard: FunctionComponent<{ } closeTokenSelectDropdowns(); }} - isMobile={isMobile} />
{`≈ ${inAmountValue || "0"}`}
@@ -531,7 +677,7 @@ export const TradeClipboard: FunctionComponent<{
diff --git a/packages/web/components/types.ts b/packages/web/components/types.ts index b8afd15ce8..e08b6abd39 100644 --- a/packages/web/components/types.ts +++ b/packages/web/components/types.ts @@ -1,4 +1,14 @@ -import { ReactElement } from "react"; +import { MouseEventHandler, ReactElement } from "react"; +import { AmplitudeEvent } from "../config"; + +export type MainLayoutMenu = { + label: string; + link: string | MouseEventHandler; + icon: string; + iconSelected?: string; + selectionTest?: RegExp; + amplitudeEvent?: AmplitudeEvent; +}; /** PROPS */ export interface InputProps { @@ -31,11 +41,16 @@ export interface MobileProps { isMobile?: boolean; } -// https://tailwindcss.com/docs/responsive-design +/** Should match settings in tailwind.config.js + * + * https://tailwindcss.com/docs/responsive-design + */ export const enum Breakpoint { SM = 640, MD = 768, LG = 1024, + XLG = 1152, XL = 1280, + XLHALF = 1408, XXL = 1536, } diff --git a/packages/web/config/feature-flag.ts b/packages/web/config/feature-flag.ts index 857ecfb2f2..513a34b3b8 100644 --- a/packages/web/config/feature-flag.ts +++ b/packages/web/config/feature-flag.ts @@ -7,6 +7,9 @@ export const UserAction: { [key: string]: boolean } = { CreateNewPool: true, }; +// Fiat ramps +export const BUY_OSMO_TRANSAK = true; + export const HiddenPoolIds: string[] = []; export const UnPoolWhitelistedPoolIds: { [poolId: string]: boolean } = { diff --git a/packages/web/config/ibc-assets.ts b/packages/web/config/ibc-assets.ts index ac471143b1..4190d6ae9b 100644 --- a/packages/web/config/ibc-assets.ts +++ b/packages/web/config/ibc-assets.ts @@ -42,6 +42,7 @@ export const IBCAssetInfos: (IBCAsset & { AxelarSourceChainConfigs.usdc.moonbeam, ], }, + fiatRamps: [{ rampKey: "kado" as const, assetKey: "USDC" }], }, { counterpartyChainId: IS_TESTNET @@ -115,7 +116,9 @@ export const IBCAssetInfos: (IBCAsset & { sourceChains: [AxelarSourceChainConfigs.wbnb.binance], wrapAssetConfig: { url: "https://pancakeswap.finance/swap?outputCurrency=0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", - displayCaption: "Convert BNB to WBNB on PancakeSwap", // TODO: use translation key instead of raw string + fromDenom: "BNB", + toDenom: "WBNB", + platformName: "PancakeSwap", }, }, }, diff --git a/packages/web/hooks/index.ts b/packages/web/hooks/index.ts index 4bcce0f4e6..d443e40f1c 100644 --- a/packages/web/hooks/index.ts +++ b/packages/web/hooks/index.ts @@ -2,7 +2,9 @@ export * from "./ui-config"; export * from "./data"; export * from "./window"; export * from "./use-deterministic"; +export * from "./use-nav-bar"; export * from "./use-keplr"; +export * from "./user-settings"; export * from "./use-boolean-with-window-event"; export * from "./use-ibc-transfer"; export * from "./use-amplitude-analytics"; diff --git a/packages/web/hooks/ui-config/index.ts b/packages/web/hooks/ui-config/index.ts index 65e1fca05c..f430605702 100644 --- a/packages/web/hooks/ui-config/index.ts +++ b/packages/web/hooks/ui-config/index.ts @@ -1,8 +1,13 @@ export * from "./use-add-liquidity-config"; export * from "./use-amount-config"; +export * from "./use-bond-liquidity-config"; export * from "./use-create-pool-config"; export * from "./use-fake-fee-config"; +export * from "./use-lock-token-config"; +export * from "./use-pool-detail-config"; +export * from "./use-pool-gauges"; export * from "./use-remove-liquidity-config"; export * from "./use-slippage-config"; +export * from "./use-superfluid-pool-config"; export * from "./use-trade-token-in-config"; export * from "./use-transfer-config"; diff --git a/packages/web/hooks/ui-config/use-add-liquidity-config.ts b/packages/web/hooks/ui-config/use-add-liquidity-config.ts index 4c7b229871..8af6f41d01 100644 --- a/packages/web/hooks/ui-config/use-add-liquidity-config.ts +++ b/packages/web/hooks/ui-config/use-add-liquidity-config.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useCallback } from "react"; import { ChainGetter, QueriesStore, @@ -9,6 +9,7 @@ import { OsmosisQueries, ObservableAddLiquidityConfig, } from "@osmosis-labs/stores"; +import { useStore } from "../../stores"; /** Maintains a single instance of `ObservableAddLiquidityConfig` for React view lifecycle. * Updates `osmosisChainId`, `poolId`, `bech32Address`, and `queryOsmosis.queryGammPoolShare` on render. @@ -17,9 +18,16 @@ export function useAddLiquidityConfig( chainGetter: ChainGetter, osmosisChainId: string, poolId: string, - bech32Address: string, queriesStore: QueriesStore<[CosmosQueries, CosmwasmQueries, OsmosisQueries]> -) { +): { + config: ObservableAddLiquidityConfig; + addLiquidity: () => Promise; +} { + const { accountStore } = useStore(); + + const account = accountStore.getAccount(osmosisChainId); + const { bech32Address } = account; + const queryOsmosis = queriesStore.get(osmosisChainId).osmosis!; const [config] = useState( () => @@ -38,5 +46,45 @@ export function useAddLiquidityConfig( config.setSender(bech32Address); config.setPoolId(poolId); config.setQueryPoolShare(queryOsmosis.queryGammPoolShare); - return config; + + const addLiquidity = useCallback(async () => { + return new Promise(async (resolve, reject) => { + try { + if (config.isSingleAmountIn && config.singleAmountInConfig) { + await account.osmosis.sendJoinSwapExternAmountInMsg( + config.poolId, + { + currency: config.singleAmountInConfig.sendCurrency, + amount: config.singleAmountInConfig.amount, + }, + undefined, + undefined, + resolve + ); + } else if (config.shareOutAmount) { + await account.osmosis.sendJoinPoolMsg( + config.poolId, + config.shareOutAmount.toDec().toString(), + undefined, + undefined, + resolve + ); + } + } catch (e: any) { + console.error(e); + reject(e.message); + } + }); + }, [ + account.osmosis, + config.isSingleAmountIn, + config.singleAmountInConfig, + config.sender, + config.poolId, + config.singleAmountInConfig?.sendCurrency, + config.singleAmountInConfig?.amount, + config.shareOutAmount, + ]); + + return { config, addLiquidity }; } diff --git a/packages/web/hooks/ui-config/use-bond-liquidity-config.ts b/packages/web/hooks/ui-config/use-bond-liquidity-config.ts new file mode 100644 index 0000000000..89a33bf84c --- /dev/null +++ b/packages/web/hooks/ui-config/use-bond-liquidity-config.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from "react"; +import { useStore } from "../../stores"; +import { ObservableBondLiquidityConfig } from "@osmosis-labs/stores"; +import { usePoolDetailConfig } from "./use-pool-detail-config"; +import { useSuperfluidPoolConfig } from "./use-superfluid-pool-config"; + +export function useBondLiquidityConfig(bech32Address: string, poolId?: string) { + const { chainStore, queriesStore, priceStore, queriesExternalStore } = + useStore(); + + const { poolDetailConfig } = usePoolDetailConfig(poolId); + const { superfluidPoolConfig } = useSuperfluidPoolConfig(poolDetailConfig); + const queryOsmosis = queriesStore.get(chainStore.osmosis.chainId).osmosis!; + + const [bondLiquidityConfig, setBondLiquidityConfig] = + useState(null); + + useEffect(() => { + if (poolDetailConfig && superfluidPoolConfig && !bondLiquidityConfig) { + setBondLiquidityConfig( + new ObservableBondLiquidityConfig( + poolDetailConfig, + superfluidPoolConfig, + priceStore, + queriesExternalStore.queryGammPoolFeeMetrics, + queryOsmosis + ) + ); + } + }, [poolDetailConfig, superfluidPoolConfig]); + + useEffect(() => { + bondLiquidityConfig?.setBech32Address(bech32Address); + }, [bondLiquidityConfig, bech32Address]); + + return bondLiquidityConfig ?? undefined; +} diff --git a/packages/web/hooks/ui-config/use-lock-token-config.ts b/packages/web/hooks/ui-config/use-lock-token-config.ts new file mode 100644 index 0000000000..253500713b --- /dev/null +++ b/packages/web/hooks/ui-config/use-lock-token-config.ts @@ -0,0 +1,121 @@ +import { useCallback } from "react"; +import { Duration } from "dayjs/plugin/duration"; +import { AmountConfig } from "@keplr-wallet/hooks"; +import { AppCurrency } from "@keplr-wallet/types"; +import { useStore } from "../../stores"; +import { useAmountConfig } from "./use-amount-config"; + +/** UI config for setting valid GAMM token amounts and un/locking them in a lock. */ +export function useLockTokenConfig(sendCurrency?: AppCurrency | undefined): { + config: AmountConfig; + lockToken: (gaugeDuration: Duration) => Promise; + unlockTokens: ( + lockIds: string[], + duration: Duration + ) => Promise<"synthetic" | "normal">; +} { + const { chainStore, queriesStore, accountStore } = useStore(); + + const { chainId } = chainStore.osmosis; + + const account = accountStore.getAccount(chainId); + const { bech32Address } = account; + + const config = useAmountConfig( + chainStore, + queriesStore, + chainId, + bech32Address, + undefined, + sendCurrency + ); + + const lockToken = useCallback( + (lockDuration) => { + return new Promise(async (resolve, reject) => { + try { + if (!config.sendCurrency.coinMinimalDenom.startsWith("gamm")) { + throw new Error("Tried to lock non-gamm token"); + } + await account.osmosis.sendLockTokensMsg( + lockDuration.asSeconds(), + [ + { + currency: config.sendCurrency, + amount: config.amount, + }, + ], + undefined, + resolve + ); + } catch (e) { + console.error(e); + reject(); + } + }); + }, + [account, config.sendCurrency, config.amount] + ); + + const queryOsmosis = queriesStore.get(chainId).osmosis!; + + const unlockTokens = useCallback( + (lockIds: string[], duration: Duration) => { + return new Promise<"synthetic" | "normal">(async (resolve, reject) => { + try { + const blockGasLimitLockIds = lockIds.slice(0, 4); + + // refresh locks + for (const lockId of blockGasLimitLockIds) { + await queryOsmosis.querySyntheticLockupsByLockId + .get(lockId) + .waitFreshResponse(); + } + + // make msg lock objects + const locks = blockGasLimitLockIds.map((lockId) => ({ + lockId, + isSyntheticLock: + queryOsmosis.querySyntheticLockupsByLockId.get(lockId) + .isSyntheticLock === true, + })); + + const durations = + queryOsmosis.queryLockableDurations.lockableDurations; + + const isSuperfluidDuration = + duration.asSeconds() === + durations[durations.length - 1]?.asSeconds(); + + if ( + isSuperfluidDuration || + locks.some((lock) => lock.isSyntheticLock) + ) { + await account.osmosis.sendBeginUnlockingMsgOrSuperfluidUnbondLockMsgIfSyntheticLock( + locks, + undefined, + () => resolve("synthetic") + ); + } else { + const blockGasLimitLockIds = lockIds.slice(0, 10); + await account.osmosis.sendBeginUnlockingMsg( + blockGasLimitLockIds, + undefined, + () => resolve("normal") + ); + } + } catch (e) { + console.error(e); + reject(); + } + }); + }, + [ + queryOsmosis, + queryOsmosis.querySyntheticLockupsByLockId, + queryOsmosis.queryLockableDurations.response, + ] + ); + + return { config, lockToken, unlockTokens }; +} diff --git a/packages/web/hooks/ui-config/use-pool-detail-config.ts b/packages/web/hooks/ui-config/use-pool-detail-config.ts new file mode 100644 index 0000000000..b9796c851c --- /dev/null +++ b/packages/web/hooks/ui-config/use-pool-detail-config.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from "react"; +import { ObservableQueryPoolDetails } from "@osmosis-labs/stores"; +import { useStore } from "../../stores"; + +export function usePoolDetailConfig(poolId?: string) { + const { chainStore, accountStore, queriesStore, priceStore } = useStore(); + + const { chainId } = chainStore.osmosis; + const queryOsmosis = queriesStore.get(chainId).osmosis!; + const account = accountStore.getAccount(chainId); + const { bech32Address } = account; + const fiat = priceStore.getFiatCurrency(priceStore.defaultVsCurrency)!; + + const pool = poolId ? queryOsmosis.queryGammPools.getPool(poolId) : undefined; + + const [poolDetailConfig, setPoolDetailConfig] = + useState(null); + useEffect(() => { + if (!poolDetailConfig && pool && fiat) { + setPoolDetailConfig( + new ObservableQueryPoolDetails(fiat, pool, queryOsmosis, priceStore) + ); + } + }, [pool, poolDetailConfig, fiat, queryOsmosis, priceStore]); + + useEffect( + () => poolDetailConfig?.setBech32Address(bech32Address), + [poolDetailConfig, bech32Address] + ); + + return { poolDetailConfig: poolDetailConfig ?? undefined, pool }; +} diff --git a/packages/web/hooks/ui-config/use-pool-gauges.ts b/packages/web/hooks/ui-config/use-pool-gauges.ts new file mode 100644 index 0000000000..2fa6100dfd --- /dev/null +++ b/packages/web/hooks/ui-config/use-pool-gauges.ts @@ -0,0 +1,102 @@ +import { useMemo } from "react"; +import { Duration } from "dayjs/plugin/duration"; +import { RatePretty, CoinPretty } from "@keplr-wallet/unit"; +import { usePoolDetailConfig } from "./use-pool-detail-config"; +import { useSuperfluidPoolConfig } from "./use-superfluid-pool-config"; +import { useStore } from "../../stores"; +import { ExternalIncentiveGaugeAllowList } from "../../config"; + +export type Gauge = { + id: string; + duration: Duration; + apr?: RatePretty; + superfluidApr?: RatePretty; + + /** Applicable to external gauges only. */ + rewardAmount?: CoinPretty; + /** Applicable to external gauges only. */ + remainingEpochs?: number; +}; + +/** Resolves all and whitelisted gauges by durations. Aggregates Gauges by duration. */ +export function usePoolGauges(poolId?: string): { + /** Aggregated by duration. */ + allAggregatedGauges: Gauge[]; + /** Aggregated by duration. */ + allowedAggregatedGauges: Gauge[]; + externalGauges: Gauge[]; + internalGauges: Gauge[]; +} { + const { chainStore } = useStore(); + + const { + osmosis: { chainId }, + } = chainStore; + + const { poolDetailConfig, pool } = usePoolDetailConfig(poolId); + const { superfluidPoolConfig } = useSuperfluidPoolConfig(poolDetailConfig); + + const allowedGauges = + pool && ExternalIncentiveGaugeAllowList[pool.id] + ? poolDetailConfig?.queryAllowedExternalGauges( + (denom) => chainStore.getChain(chainId).findCurrency(denom), + ExternalIncentiveGaugeAllowList[pool.id] + ) ?? [] + : []; + const externalGauges = poolDetailConfig?.allExternalGauges ?? []; + const allAggregatedGauges: Gauge[] | undefined = useMemo(() => { + const gaugeDurationMap = new Map(); + + // uniqued external gauges by duration + externalGauges.concat(allowedGauges).forEach((extGauge) => { + gaugeDurationMap.set(extGauge.duration.asSeconds(), { + id: extGauge.id, + duration: extGauge.duration, + rewardAmount: extGauge.rewardAmount, + remainingEpochs: extGauge.remainingEpochs, + }); + }); + + // overwrite any external gauges with internal gauges w/ apr calcs + superfluidPoolConfig?.gaugesWithSuperfluidApr.forEach((gauge) => { + gaugeDurationMap.set(gauge.duration.asSeconds(), gauge); + }); + + return Array.from(gaugeDurationMap.values()).sort( + (a, b) => a.duration.asSeconds() - b.duration.asSeconds() + ); + }, [ + allowedGauges, + externalGauges, + superfluidPoolConfig?.gaugesWithSuperfluidApr, + ]); + const allowedAggregatedGauges = useMemo(() => { + const gaugeDurationMap = new Map(); + + // uniqued external gauges by duration + allowedGauges.forEach((extGauge) => { + gaugeDurationMap.set(extGauge.duration.asSeconds(), { + id: extGauge.id, + duration: extGauge.duration, + rewardAmount: extGauge.rewardAmount, + remainingEpochs: extGauge.remainingEpochs, + }); + }); + + // overwrite any external gauges with internal gauges w/ apr calcs + superfluidPoolConfig?.gaugesWithSuperfluidApr.forEach((gauge) => { + gaugeDurationMap.set(gauge.duration.asSeconds(), gauge); + }); + + return Array.from(gaugeDurationMap.values()).sort( + (a, b) => a.duration.asSeconds() - b.duration.asSeconds() + ); + }, [allowedGauges, superfluidPoolConfig?.gaugesWithSuperfluidApr]); + + return { + allAggregatedGauges, + allowedAggregatedGauges, + internalGauges: poolDetailConfig?.internalGauges ?? [], + externalGauges, + }; +} diff --git a/packages/web/hooks/ui-config/use-remove-liquidity-config.ts b/packages/web/hooks/ui-config/use-remove-liquidity-config.ts index 73a211ce86..e5d83c008e 100644 --- a/packages/web/hooks/ui-config/use-remove-liquidity-config.ts +++ b/packages/web/hooks/ui-config/use-remove-liquidity-config.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useCallback } from "react"; import { ChainGetter, QueriesStore, @@ -9,6 +9,7 @@ import { OsmosisQueries, ObservableRemoveLiquidityConfig, } from "@osmosis-labs/stores"; +import { useStore } from "../../stores"; /** Maintains a single instance of `ObservableRemoveLiquidityConfig` for React view lifecycle. * Updates `osmosisChainId`, `poolId`, `bech32Address`, and `queryOsmosis.queryGammPoolShare` on render. @@ -18,10 +19,17 @@ export function useRemoveLiquidityConfig( chainGetter: ChainGetter, osmosisChainId: string, poolId: string, - bech32Address: string, queriesStore: QueriesStore<[CosmosQueries, CosmwasmQueries, OsmosisQueries]>, initialPercent = "50" -) { +): { + config: ObservableRemoveLiquidityConfig; + removeLiquidity: () => Promise; +} { + const { accountStore } = useStore(); + + const account = accountStore.getAccount(osmosisChainId); + const { bech32Address } = account; + const queryOsmosis = queriesStore.get(osmosisChainId).osmosis!; const [config] = useState(() => { const c = new ObservableRemoveLiquidityConfig( @@ -41,5 +49,23 @@ export function useRemoveLiquidityConfig( config.setSender(bech32Address); config.setPoolId(poolId); config.setQueryPoolShare(queryOsmosis.queryGammPoolShare); - return config; + + const removeLiquidity = useCallback(() => { + return new Promise(async (resolve, reject) => { + try { + await account.osmosis.sendExitPoolMsg( + config.poolId, + config.poolShareWithPercentage.toDec().toString(), + undefined, + undefined, + resolve + ); + } catch (e) { + console.error(e); + reject(); + } + }); + }, []); + + return { config, removeLiquidity }; } diff --git a/packages/web/hooks/ui-config/use-superfluid-pool-config.ts b/packages/web/hooks/ui-config/use-superfluid-pool-config.ts new file mode 100644 index 0000000000..49670bf7b6 --- /dev/null +++ b/packages/web/hooks/ui-config/use-superfluid-pool-config.ts @@ -0,0 +1,108 @@ +import { useState, useEffect, useCallback } from "react"; +import { + ObservableQueryPoolDetails, + ObservableQuerySuperfluidPool, +} from "@osmosis-labs/stores"; +import { useStore } from "../../stores"; +import { AmountConfig } from "@keplr-wallet/hooks"; + +/** When provided a pool details store (which may need to be loaded), will generate superfluid pool info and actions. */ +export function useSuperfluidPoolConfig( + poolDetails?: ObservableQueryPoolDetails +): { + superfluidPoolConfig: ObservableQuerySuperfluidPool | undefined; + superfluidDelegateToValidator: ( + validatorAddress: string, + lockLPTokensConfig?: AmountConfig + ) => Promise<"delegated" | "locked-and-delegated">; +} { + const { chainStore, accountStore, queriesStore, priceStore } = useStore(); + const { chainId } = chainStore.osmosis; + + const account = accountStore.getAccount(chainId); + const { bech32Address } = account; + const fiat = priceStore.getFiatCurrency(priceStore.defaultVsCurrency)!; + const queryOsmosis = queriesStore.get(chainId).osmosis!; + + const [superfluidPoolConfig, setSuperfluidPoolStore] = + useState(null); + useEffect(() => { + if (poolDetails && !superfluidPoolConfig) { + setSuperfluidPoolStore( + new ObservableQuerySuperfluidPool( + fiat, + poolDetails, + queriesStore.get(chainId).cosmos.queryValidators, + queriesStore.get(chainId).cosmos.queryInflation, + queryOsmosis, + priceStore + ) + ); + } + }, [poolDetails, fiat, queryOsmosis, priceStore]); + + useEffect( + () => superfluidPoolConfig?.setBech32Address(bech32Address), + [superfluidPoolConfig, bech32Address] + ); + + const superfluidDelegateToValidator = useCallback( + (validatorAddress, lockLPTokensConfig) => { + return new Promise<"delegated" | "locked-and-delegated">( + async (resolve, reject) => { + if (superfluidPoolConfig?.superfluid) { + if (superfluidPoolConfig.superfluid.upgradeableLpLockIds) { + // is delegating existing locked shares + try { + await account.osmosis.sendSuperfluidDelegateMsg( + superfluidPoolConfig.superfluid.upgradeableLpLockIds.lockIds, + validatorAddress, + undefined, + () => resolve("delegated") + ); + } catch (e) { + console.error(e); + reject(); + } + } else if ( + superfluidPoolConfig.superfluid.superfluidLpShares && + lockLPTokensConfig + ) { + try { + await account.osmosis.sendLockAndSuperfluidDelegateMsg( + [ + { + currency: lockLPTokensConfig.sendCurrency, + amount: lockLPTokensConfig.amount, + }, + ], + validatorAddress, + undefined, + () => resolve("locked-and-delegated") + ); + } catch (e) { + console.error(e); + reject(); + } + } else { + console.warn( + "Superfluid delegate: amount config for use in sendLockAndSuperfluidDelegateMsg missing" + ); + } + } + } + ); + }, + [ + superfluidPoolConfig?.superfluid, + superfluidPoolConfig?.superfluid?.upgradeableLpLockIds, + superfluidPoolConfig?.superfluid?.upgradeableLpLockIds?.lockIds, + superfluidPoolConfig?.superfluid?.superfluidLpShares, + ] + ); + + return { + superfluidPoolConfig: superfluidPoolConfig ?? undefined, + superfluidDelegateToValidator, + }; +} diff --git a/packages/web/hooks/use-amplitude-analytics.ts b/packages/web/hooks/use-amplitude-analytics.ts index 39dcd43fad..7414e9aab9 100644 --- a/packages/web/hooks/use-amplitude-analytics.ts +++ b/packages/web/hooks/use-amplitude-analytics.ts @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useCallback, useEffect } from "react"; import { init as amplitudeInit, identify, @@ -17,20 +17,26 @@ export function useAmplitudeAnalytics({ /** Init analytics environment. Done once per user session. */ init?: true; } = {}) { - const logEvent = ([eventName, eventProperties]: - | [string, Partial> | undefined] - | [string]) => { - amplitudeLogEvent(eventName, eventProperties); - }; + const logEvent = useCallback( + ([eventName, eventProperties]: + | [string, Partial> | undefined] + | [string]) => { + amplitudeLogEvent(eventName, eventProperties); + }, + [] + ); - const setUserProperty = ( - key: keyof UserProperties, - value: UserProperties[keyof UserProperties] - ) => { - const newIdentify = new Identify(); - newIdentify.set(key, value); - identify(newIdentify); - }; + const setUserProperty = useCallback( + ( + key: keyof UserProperties, + value: UserProperties[keyof UserProperties] + ) => { + const newIdentify = new Identify(); + newIdentify.set(key, value); + identify(newIdentify); + }, + [] + ); useEffect(() => { if (init) { diff --git a/packages/web/hooks/use-keplr/use-connect-wallet-modal-redirect.tsx b/packages/web/hooks/use-keplr/use-connect-wallet-modal-redirect.tsx index bde045eaf7..93f999b74e 100644 --- a/packages/web/hooks/use-keplr/use-connect-wallet-modal-redirect.tsx +++ b/packages/web/hooks/use-keplr/use-connect-wallet-modal-redirect.tsx @@ -4,6 +4,7 @@ import { WalletStatus } from "@keplr-wallet/stores"; import { useStore } from "../../stores"; import { Button } from "../../components/buttons"; import { useKeplr } from "./hook"; +import { t } from "react-multi-lang"; /** FOR USE IN MODALS * @@ -21,7 +22,7 @@ import { useKeplr } from "./hook"; export function useConnectWalletModalRedirect( actionButtonProps: ComponentProps, onRequestClose: () => void, - connectWalletMessage = "Connect wallet" + connectWalletMessage = t("connectWallet") ) { const keplr = useKeplr(); const { accountStore, chainStore } = useStore(); @@ -60,10 +61,8 @@ export function useConnectWalletModalRedirect( ) : ( )}
diff --git a/packages/web/integrations/axelar/types.ts b/packages/web/integrations/axelar/types.ts index 4f5f7c9e48..de9e4d2049 100644 --- a/packages/web/integrations/axelar/types.ts +++ b/packages/web/integrations/axelar/types.ts @@ -18,7 +18,9 @@ export interface AxelarBridgeConfig { /** URL config for users to conveniently swap the native asset for the wrapped version. */ wrapAssetConfig?: { url: string; - displayCaption: string; + fromDenom: string; + toDenom: string; + platformName: string; }; } diff --git a/packages/web/integrations/axelar/utils.ts b/packages/web/integrations/axelar/utils.ts index 661a148638..5113e3ddba 100644 --- a/packages/web/integrations/axelar/utils.ts +++ b/packages/web/integrations/axelar/utils.ts @@ -1,11 +1,12 @@ import { SourceChain } from "./types"; +import { t } from "react-multi-lang"; export function waitBySourceChain(sourceChain: SourceChain) { switch (sourceChain) { case "Ethereum": case "Polygon": - return "15 minutes"; + return t("assets.transfer.waitTime", { minutes: "15" }); default: - return "3 minutes"; + return t("assets.transfer.waitTime", { minutes: "3" }); } } diff --git a/packages/web/integrations/bridge-info.ts b/packages/web/integrations/bridge-info.ts index 5be5c09ab3..67cf2d66e6 100644 --- a/packages/web/integrations/bridge-info.ts +++ b/packages/web/integrations/bridge-info.ts @@ -10,3 +10,19 @@ export type OriginBridgeInfo = { /** String literal identifiers for a source chain. */ export type SourceChainKey = SourceChain; + +// Fiat on/off ramps + +export type FiatRampKey = "kado" | "transak"; +export const FiatRampDisplayInfos: { + [key: string]: { iconUrl: string; displayName: string }; +} = { + kado: { + iconUrl: "/logos/kado.svg", + displayName: "Kado", + }, + transak: { + iconUrl: "/logos/transak.svg", + displayName: "Transak", + }, +}; diff --git a/packages/web/integrations/ethereum/metamask-utils.ts b/packages/web/integrations/ethereum/metamask-utils.ts index 64ce8dae93..e106fce55c 100644 --- a/packages/web/integrations/ethereum/metamask-utils.ts +++ b/packages/web/integrations/ethereum/metamask-utils.ts @@ -84,9 +84,6 @@ export function withEthInWindow( ) { return doTask(window.ethereum); } - if (typeof window !== "undefined") { - console.warn("MetaMask: no window.ethereum found"); - } return defaultRet; } diff --git a/packages/web/integrations/kado/index.tsx b/packages/web/integrations/kado/index.tsx new file mode 100644 index 0000000000..42bd42f563 --- /dev/null +++ b/packages/web/integrations/kado/index.tsx @@ -0,0 +1,23 @@ +import { FunctionComponent } from "react"; +import { WalletStatus } from "@keplr-wallet/stores"; +import { useStore } from "../../stores"; +import { ModalBaseProps } from "../../modals"; + +/** Assumed wallet connected */ +export const Kado: FunctionComponent< + { assetKey: string } & Pick +> = ({ assetKey }) => { + const { chainStore, accountStore } = useStore(); + + const account = accountStore.getAccount(chainStore.osmosis.chainId); + + if (!(account.walletStatus === WalletStatus.Loaded)) return null; + + return ( +