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 (
+
+ )
+}
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 (
+
+
+
+ {header.column.getCanSort()
+ ? {
+ asc: ,
+ desc: ,
+ false: ,
+ }[header.column.getIsSorted() as string] ?? null
+ : null}
+
+
+ {flexRender(header.column.columnDef.header, header.getContext())}
+
+
+ |
+ )
+ })}
+
+ ))}
+
+
+ {table.getRowModel().rows.map((row) => {
+ if (row.getIsExpanded()) {
+ return (
+
+
+
+ )
+ }
+ return (
+
+ )
+ })}
+
+
+ )
+}
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 }))
+}