From ac09862f1fbd0fb6c97b09899696eac82cbf4084 Mon Sep 17 00:00:00 2001 From: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com> Date: Sun, 16 Apr 2023 11:03:48 +0200 Subject: [PATCH] Farm components (#158) * add basic endpoint for vaultConfigs * implement apy for vaults * add tab + update routing * fixed routing issues * add featured vaults and vault card * add availablevaults table * fixed comments --- package-lock.json | 84 ++++++++++++ src/components/Earn/AvailableVaults.tsx | 38 ++++++ src/components/Earn/FeaturedVaults.tsx | 36 +++++ src/components/Earn/Overview.tsx | 40 ------ src/components/Earn/VaultCard.tsx | 68 ++++++++++ src/components/Earn/VaultLogo.tsx | 25 ++++ src/components/Earn/VaultRow.tsx | 33 +++++ src/components/Earn/VaultTable.tsx | 170 ++++++++++++++++++++++++ src/constants/vaults.ts | 3 + src/types/interfaces/vaults.d.ts | 1 + src/utils/api.ts | 2 +- src/utils/assets.ts | 2 +- src/utils/formatters.ts | 7 + 13 files changed, 467 insertions(+), 42 deletions(-) create mode 100644 src/components/Earn/AvailableVaults.tsx create mode 100644 src/components/Earn/FeaturedVaults.tsx delete mode 100644 src/components/Earn/Overview.tsx create mode 100644 src/components/Earn/VaultCard.tsx create mode 100644 src/components/Earn/VaultLogo.tsx create mode 100644 src/components/Earn/VaultRow.tsx create mode 100644 src/components/Earn/VaultTable.tsx diff --git a/package-lock.json b/package-lock.json index 0756f5a0..cab0200c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19229,6 +19229,66 @@ "optional": true } } + }, + "node_modules/@next/swc-android-arm-eabi": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz", + "integrity": "sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-android-arm64": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz", + "integrity": "sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-freebsd-x64": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz", + "integrity": "sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm-gnueabihf": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz", + "integrity": "sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } } }, "dependencies": { @@ -33300,6 +33360,30 @@ "requires": { "use-sync-external-store": "1.2.0" } + }, + "@next/swc-android-arm-eabi": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz", + "integrity": "sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==", + "optional": true + }, + "@next/swc-android-arm64": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz", + "integrity": "sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==", + "optional": true + }, + "@next/swc-freebsd-x64": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz", + "integrity": "sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==", + "optional": true + }, + "@next/swc-linux-arm-gnueabihf": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz", + "integrity": "sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==", + "optional": true } } } diff --git a/src/components/Earn/AvailableVaults.tsx b/src/components/Earn/AvailableVaults.tsx new file mode 100644 index 00000000..0d598676 --- /dev/null +++ b/src/components/Earn/AvailableVaults.tsx @@ -0,0 +1,38 @@ +import { Suspense } from 'react' + +import Card from 'components/Card' +import { getVaults } from 'utils/api' +import { Text } from 'components/Text' +import { VAULTS } from 'constants/vaults' + +import { VaultTable } from './VaultTable' + +async function Content() { + const vaults = await getVaults() + + if (!vaults.length) return null + + return +} + +export default function AvailableVaults() { + return ( + + }> + {/* @ts-expect-error Server Component */} + + + + ) +} + +function Fallback() { + // TODO: Replace with loading state of vaulttable + return ( + <> + {VAULTS.map((vault) => ( + {vault.name} + ))} + + ) +} diff --git a/src/components/Earn/FeaturedVaults.tsx b/src/components/Earn/FeaturedVaults.tsx new file mode 100644 index 00000000..b4b9b466 --- /dev/null +++ b/src/components/Earn/FeaturedVaults.tsx @@ -0,0 +1,36 @@ +import { Suspense } from 'react' + +import Card from 'components/Card' +import { getVaults } from 'utils/api' +import { Text } from 'components/Text' + +import VaultCard from './VaultCard' + +async function Content() { + const vaults = await getVaults() + + const featuredVaults = vaults.filter((vault) => vault.isFeatured) + + if (!featuredVaults.length) return null + + return ( + + {featuredVaults.map((vault) => ( + + ))} + + ) +} + +export default function FeaturedVaults() { + return ( + + {/* @ts-expect-error Server Component */} + + + ) +} diff --git a/src/components/Earn/Overview.tsx b/src/components/Earn/Overview.tsx deleted file mode 100644 index a48471f8..00000000 --- a/src/components/Earn/Overview.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Suspense } from 'react' - -import Card from 'components/Card' -import Loading from 'components/Loading' -import { Text } from 'components/Text' -import { getVaults } from 'utils/api' - -async function Content(props: PageProps) { - const vaults = await getVaults() - - const address = props.params.address - - if (!address) - return ( - - You need to be connected to use the earn page - - ) - - return {`Earn page for ${address}`} -} - -function Fallback() { - return -} - -export default function Overview(props: PageProps) { - return ( - - }> - {/* @ts-expect-error Server Component */} - - - - ) -} diff --git a/src/components/Earn/VaultCard.tsx b/src/components/Earn/VaultCard.tsx new file mode 100644 index 00000000..04318378 --- /dev/null +++ b/src/components/Earn/VaultCard.tsx @@ -0,0 +1,68 @@ +'use client' + +import Image from 'next/image' + +import { Text } from 'components/Text' +import { getAssetByDenom } from 'utils/assets' +import TitleAndSubCell from 'components/TitleAndSubCell' +import { formatPercent, formatValue } from 'utils/formatters' +import { Button } from 'components/Button' +import VaultLogo from 'components/Earn/VaultLogo' + +interface Props { + vault: Vault +} + +export default function VaultCard(props: Props) { + function openVaultModal() {} + + return ( +
+
+
+ + Hot off the presses + + + {props.vault.name} + + via {props.vault.provider} + + +
+ +
+
+ + + + +
+ +
+ ) +} diff --git a/src/components/Earn/VaultLogo.tsx b/src/components/Earn/VaultLogo.tsx new file mode 100644 index 00000000..3e32b4a9 --- /dev/null +++ b/src/components/Earn/VaultLogo.tsx @@ -0,0 +1,25 @@ +import Image from 'next/image' + +import { getAssetByDenom } from 'utils/assets' + +interface Props { + vault: Vault +} + +export default function VaultLogo(props: Props) { + const primaryAsset = getAssetByDenom(props.vault.denoms.primary) + const secondaryAsset = getAssetByDenom(props.vault.denoms.secondary) + + if (!primaryAsset || !secondaryAsset) return null + + return ( +
+
+ {`${primaryAsset.symbol} +
+
+ token +
+
+ ) +} diff --git a/src/components/Earn/VaultRow.tsx b/src/components/Earn/VaultRow.tsx new file mode 100644 index 00000000..1440da67 --- /dev/null +++ b/src/components/Earn/VaultRow.tsx @@ -0,0 +1,33 @@ +import { flexRender, Row } from '@tanstack/react-table' +import classNames from 'classnames' + +type AssetRowProps = { + row: Row + resetExpanded: (defaultState?: boolean | undefined) => void +} + +export const VaultRow = (props: AssetRowProps) => { + return ( + { + e.preventDefault() + const isExpanded = props.row.getIsExpanded() + props.resetExpanded() + !isExpanded && props.row.toggleExpanded() + }} + > + {props.row.getVisibleCells().map((cell, index) => { + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) + })} + + ) +} diff --git a/src/components/Earn/VaultTable.tsx b/src/components/Earn/VaultTable.tsx new file mode 100644 index 00000000..72fe342b --- /dev/null +++ b/src/components/Earn/VaultTable.tsx @@ -0,0 +1,170 @@ +'use client' + +import { + ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + SortingState, + useReactTable, +} from '@tanstack/react-table' +import classNames from 'classnames' +import React from 'react' + +import { ChevronDown, SortAsc, SortDesc, SortNone } from 'components/Icons' +import { Text } from 'components/Text' +import { getAssetByDenom, getMarketAssets } from 'utils/assets' +import VaultLogo from 'components/Earn/VaultLogo' +import TitleAndSubCell from 'components/TitleAndSubCell' +import { convertPercentage, formatPercent, formatValue } from 'utils/formatters' +import { VAULT_DEPOSIT_BUFFER } from 'constants/vaults' + +import { VaultRow } from 'components/Earn/VaultRow' + +type Props = { + data: Vault[] +} + +export const VaultTable = (props: Props) => { + const [sorting, setSorting] = React.useState([]) + const marketAssets = getMarketAssets() + + const columns = React.useMemo[]>( + () => [ + { + header: 'Vault', + id: 'address', + cell: ({ row }) => { + return + }, + }, + { + accessorKey: 'apy', + header: 'APY', + cell: ({ row }) => { + return {row.original.apy ? formatPercent(row.original.apy) : '-'} + }, + }, + { + accessorKey: 'tvl', + header: 'TVL', + cell: ({ row }) => { + // TODO: Replace with DisplayCurrency + const symbol = getAssetByDenom(row.original.cap.denom)?.symbol ?? '' + return ( + + {formatValue(row.original.cap.used, { abbreviated: true, suffix: ` ${symbol}` })} + + ) + }, + }, + { + accessorKey: 'cap', + header: 'Depo. Cap', + cell: ({ row }) => { + const percent = convertPercentage( + (row.original.cap.used / (row.original.cap.max * VAULT_DEPOSIT_BUFFER)) * 100, + ) + const decimals = getAssetByDenom(row.original.cap.denom)?.decimals ?? 6 + + // TODO: Replace with DisplayCurrency + return ( + + ) + }, + }, + { + accessorKey: 'cap', + enableSorting: false, + header: 'Details', + cell: ({ row }) => ( +
+
+ +
+
+ ), + }, + ], + [marketAssets, props.data], + ) + + const table = useReactTable({ + data: props.data, + columns, + state: { + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + debugTable: true, + }) + + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header, index) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + if (row.getIsExpanded()) { + return ( + + + + ) + } + return ( + + ) + })} + +
+
+ + {header.column.getCanSort() + ? { + asc: , + desc: , + false: , + }[header.column.getIsSorted() as string] ?? null + : null} + + + {flexRender(header.column.columnDef.header, header.getContext())} + +
+
+ ) +} diff --git a/src/constants/vaults.ts b/src/constants/vaults.ts index bbf334fa..f9b272e0 100644 --- a/src/constants/vaults.ts +++ b/src/constants/vaults.ts @@ -1,3 +1,5 @@ +export const VAULT_DEPOSIT_BUFFER = 0.999 + export const VAULTS: VaultMetaData[] = [ { address: 'osmo108q2krqr0y9g0rtesenvsw68sap2xefelwwjs0wedyvdl0cmrntqvllfjk', @@ -16,6 +18,7 @@ export const VAULTS: VaultMetaData[] = [ primary: 'OSMO', secondary: 'ATOM', }, + isFeatured: true, }, { address: 'osmo1g5hryv0gp9dzlchkp3yxk8fmcf5asjun6cxkvyffetqzkwmvy75qfmeq3f', diff --git a/src/types/interfaces/vaults.d.ts b/src/types/interfaces/vaults.d.ts index fe3fc910..aa943627 100644 --- a/src/types/interfaces/vaults.d.ts +++ b/src/types/interfaces/vaults.d.ts @@ -15,6 +15,7 @@ interface VaultMetaData { primary: string secondary: string } + isFeatured?: boolean } interface VaultConfig extends VaultMetaData { diff --git a/src/utils/api.ts b/src/utils/api.ts index 38451e0f..345b1361 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -65,7 +65,7 @@ export async function getPrices() { } export async function getVaults() { - return callAPI(getEndpoint(Endpoints.VAULTS), 'default') + return callAPI(getEndpoint(Endpoints.VAULTS), 'default') } export async function getWalletBalancesSWR(url: string) { diff --git a/src/utils/assets.ts b/src/utils/assets.ts index 85b062ac..233b14bd 100644 --- a/src/utils/assets.ts +++ b/src/utils/assets.ts @@ -1,6 +1,6 @@ import { ASSETS } from 'constants/assets' -export function getAssetByDenom(denom: string) { +export function getAssetByDenom(denom: string): Asset | undefined { return ASSETS.find((asset) => asset.denom === denom) } diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index 633a4afd..02aed239 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -136,3 +136,10 @@ export function formatAmountWithSymbol(coin: Coin) { rounded: true, }) } + +export const convertPercentage = (percent: number) => { + let percentage = percent + if (percent >= 100) percentage = 100 + if (percent !== 0 && percent < 0.01) percentage = 0.01 + return Number(formatValue(percentage, { minDecimals: 0, maxDecimals: 0 })) +}