New architecture (#196)

* feat: move app to pages features

* feat: route changes

* Use React Router, Remove SSR

* Fix account menu

* Remove app folder

* remove old useParams

* Moved pages back to pages and refactor names

* add layout to route

* clean up

* create hooks for api fetching

* fix refetch of all data on tx complete

* formatting

* fix: fixed the wallet-connector race condition

* remove cosmjs/stargate (#202)

* remove cosmjs/stargate

* add Yusuf as code-orwner

* Singleton client (#203)

* remove cosmjs/stargate

* add Yusuf as code-orwner

* create signleton client and refactor vaults api

* update client name, add apollo apr env

* Setup validate-env and remove checking from apis

* uncomment vaults

* Resolve comments

* fix: html templating, add checks for hydration&window object, reduce bundle size (#204)

* fix: tests

* Fix routing and wallet client (#205)

* Add header to router (as layout)

* Refactor Wallet component

* Remove server fallback packages webpack

* add missing dependency for useeffect

---------

Co-authored-by: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com>
Co-authored-by: Linkie Link <linkielink.dev@gmail.com>
This commit is contained in:
Yusuf Seyrek 2023-05-16 13:39:52 +03:00 committed by GitHub
parent a59e880559
commit b24bbb3376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
137 changed files with 1039 additions and 1211 deletions

View File

@ -4,6 +4,7 @@ NEXT_PUBLIC_RPC=https://testnet-osmosis-node.marsprotocol.io/XF32UOOU55CX/osmosi
NEXT_PUBLIC_GQL=https://testnet-osmosis-node.marsprotocol.io/XF32UOOU55CX/osmosis-hive-front/graphql
NEXT_PUBLIC_REST=https://testnet-osmosis-node.marsprotocol.io/XF32UOOU55CX/osmosis-lcd-front/
NEXT_PUBLIC_SWAP=https://testnet.osmosis.zone
NEXT_PUBLIC_APOLLO_APR=https://api.apollo.farm/api/vault_infos/v2/osmo-test-5
NEXT_PUBLIC_WALLETS=keplr,cosmostation
NEXT_PUBLIC_ACCOUNT_NFT=osmo1ye2rntzz9qmxgv7eg09supww6k6xs0y0sekcr3x5clp087fymn4q3y33s4
NEXT_PUBLIC_ORACLE=osmo1khe29uw3t85nmmp3mtr8dls7v2qwsfk3tndu5h4w5g2r5tzlz5qqarq2e2
@ -21,6 +22,7 @@ NEXT_PUBLIC_API=http://localhost:3000/api
# NEXT_PUBLIC_GQL=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-hive-front/graphql
# NEXT_PUBLIC_REST=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-lcd-front/
# NEXT_PUBLIC_SWAP=https://app.osmosis.zone
# NEXT_PUBLIC_APOLLO_APR=https://api.apollo.farm/api/vault_infos/v2/osmosis-1
# NEXT_PUBLIC_WALLETS=keplr,xfi-cosmos,leap-cosmos,cosmostation,mobile-keplr,mobile-cosmostation
# NEXT_PUBLIC_ACCOUNT_NFT=osmo1450hrg6dv2l58c0rvdwx8ec2a0r6dd50hn4frk370tpvqjhy8khqw7sw09
# NEXT_PUBLIC_ORACLE=osmo1mhznfr60vjdp2gejhyv2gax9nvyyzhd3z0qcwseyetkfustjauzqycsy2g

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @bobthebuidlr @linkielink
* @bobthebuidlr @linkielink @yusufseyrek

View File

@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react'
import * as rrd from 'react-router-dom'
import * as useParams from 'utils/route'
import AccountDetails from 'components/Account/AccountDetails'
jest.mock('utils/route')
const mockedUseParams = useParams.default as jest.Mock
jest.mock('react-router-dom')
const mockedUseParams = rrd.useParams as jest.Mock
describe('<AccountDetails />', () => {
afterAll(() => {

View File

@ -1,46 +1,16 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
experimental: {
appDir: true,
},
reactStrictMode: true,
async redirects() {
async rewrites() {
return [
{
source: '/',
destination: '/trade',
permanent: true,
},
{
source: '/wallets',
destination: '/trade',
permanent: true,
},
{
source: '/wallets/:wallet',
destination: '/wallets/:wallet/trade',
permanent: true,
},
{
source: '/wallets/:wallet/accounts',
destination: '/wallets/:wallet/accounts/trade',
permanent: true,
source: '/:any*',
destination: '/',
},
]
},
webpack(config, { isServer }) {
if (isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
'utf-8-validate': false,
bufferutil: false,
'./build/Release/ecdh': false,
eccrypto: false,
}
}
webpack(config) {
config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,

84
package-lock.json generated
View File

@ -19152,6 +19152,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": {
@ -33215,6 +33275,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
}
}
}

View File

@ -3,18 +3,18 @@
"version": "2.0.0",
"private": true,
"scripts": {
"build": "next build",
"build": "yarn validate-env && next build",
"dev": "next dev",
"test": "jest",
"lint": "eslint ./src/ && yarn prettier-check",
"format": "eslint ./src/ --fix && prettier --write ./src/",
"prettier-check": "prettier --ignore-path .gitignore --check ./src/",
"start": "next start"
"start": "next start",
"validate-env": "node ./validate-env"
},
"dependencies": {
"@cosmjs/cosmwasm-stargate": "^0.30.1",
"@cosmjs/stargate": "^0.30.1",
"@marsprotocol/wallet-connector": "^1.5.8",
"@marsprotocol/wallet-connector": "^1.5.9",
"@sentry/nextjs": "^7.51.2",
"@tanstack/react-table": "^8.9.1",
"@tippyjs/react": "^4.2.6",
@ -27,6 +27,7 @@
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.5",
"react-router-dom": "^6.11.1",
"react-spring": "^9.7.1",
"react-toastify": "^9.1.2",
"react-use-clipboard": "^1.0.9",
@ -37,15 +38,16 @@
},
"devDependencies": {
"@svgr/webpack": "^8.0.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/node": "^20.1.1",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"autoprefixer": "^10.4.14",
"babel-jest": "^29.5.0",
"dotenv": "^16.0.3",
"eslint": "8.40.0",
"eslint-config-next": "^13.4.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"babel-jest": "^29.5.0",
"eslint-plugin-import": "^2.27.5",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.5.0",

View File

@ -0,0 +1,18 @@
import { getClient } from 'api/cosmwasm-client'
import { ENV } from 'constants/env'
export default async function getAccount(accountId: string): Promise<AccountResponse> {
const client = await getClient()
const account: AccountResponse = await client.queryContractSmart(ENV.ADDRESS_CREDIT_MANAGER, {
positions: {
account_id: accountId,
},
})
if (account) {
return account
}
return new Promise((_, reject) => reject('No account found'))
}

View File

@ -0,0 +1,11 @@
import getAccount from 'api/accounts/getAccount'
export default async function getAccountDebts(accountId: string): Promise<Coin[]> {
const account = await getAccount(accountId)
if (account) {
return account.debts
}
return new Promise((_, reject) => reject('Account not found'))
}

View File

@ -0,0 +1,11 @@
import getAccount from 'api/accounts/getAccount'
export default async function getAccountDeposits(accountId: string) {
const account = await getAccount(accountId)
if (account) {
return account.deposits
}
return new Promise((_, reject) => reject('Account not found'))
}

View File

@ -0,0 +1,11 @@
import getAccount from 'api/accounts/getAccount'
export default async function getAccountDeposits(accountId: string) {
const account = await getAccount(accountId)
if (account) {
return account.vaults
}
return new Promise((_, reject) => reject('Account not found'))
}

View File

@ -0,0 +1,19 @@
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { ENV } from 'constants/env'
let _cosmWasmClient: CosmWasmClient
const getClient = async () => {
try {
if (!_cosmWasmClient) {
_cosmWasmClient = await CosmWasmClient.connect(ENV.URL_RPC || '')
}
return _cosmWasmClient
} catch (error) {
throw error
}
}
export { getClient }

View File

@ -1,13 +1,8 @@
import { gql, request as gqlRequest } from 'graphql-request'
import { NextApiRequest, NextApiResponse } from 'next'
import { ENV, ENV_MISSING_MESSAGE } from 'constants/env'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!ENV.URL_GQL || !ENV.ADDRESS_RED_BANK) {
return res.status(404).json(ENV_MISSING_MESSAGE)
}
import { ENV } from 'constants/env'
export default async function getBalances() {
const result = await gqlRequest<Result>(
ENV.URL_GQL,
gql`
@ -24,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
`,
)
return res.status(200).json(result.bank.balance)
return result.bank.balance
}
interface Result {

View File

@ -0,0 +1,29 @@
import { BN } from 'utils/helpers'
import getPrices from 'api/prices/getPrices'
import getMarkets from 'api/markets/getMarkets'
import getMarketLiquidity from 'api/markets/getMarketLiquidity'
export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
const liquidity = await getMarketLiquidity()
const borrowEnabledMarkets = (await getMarkets()).filter((market: Market) => market.borrowEnabled)
const prices = await getPrices()
const borrow: BorrowAsset[] = borrowEnabledMarkets.map((market) => {
const price = prices.find((coin) => coin.denom === market.denom)?.amount ?? '1'
const amount = liquidity.find((coin) => coin.denom === market.denom)?.amount ?? '0'
return {
denom: market.denom,
borrowRate: market.borrowRate ?? 0,
liquidity: {
amount: amount,
value: BN(amount).times(price).toString(),
},
}
})
if (borrow) {
return borrow
}
return new Promise((_, reject) => reject('No data'))
}

View File

@ -1,15 +1,11 @@
import { gql, request as gqlRequest } from 'graphql-request'
import { NextApiRequest, NextApiResponse } from 'next'
import { ENV, ENV_MISSING_MESSAGE, VERCEL_BYPASS } from 'constants/env'
import { ENV } from 'constants/env'
import { denomToKey, getContractQuery, keyToDenom } from 'utils/query'
import getMarkets from 'api/markets/getMarkets'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!ENV.URL_API || !ENV.ADDRESS_RED_BANK || !ENV.URL_GQL) {
return res.status(404).json(ENV_MISSING_MESSAGE)
}
const markets: Market[] = await (await fetch(`${ENV.URL_API}/markets${VERCEL_BYPASS}`)).json()
export default async function getMarketDebts(): Promise<Coin[]> {
const markets: Market[] = await getMarkets()
let query = ''
@ -45,10 +41,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
amount: result.debts[key],
}
})
return res.status(200).json(debts)
return debts
}
return res.status(404)
return new Promise((_, reject) => reject('No data'))
}
interface DebtsQuery {

View File

@ -1,15 +1,11 @@
import { gql, request as gqlRequest } from 'graphql-request'
import { NextApiRequest, NextApiResponse } from 'next'
import { ENV, ENV_MISSING_MESSAGE, VERCEL_BYPASS } from 'constants/env'
import { ENV } from 'constants/env'
import { denomToKey, getContractQuery, keyToDenom } from 'utils/query'
import getMarkets from 'api/markets/getMarkets'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!ENV.URL_RPC || !ENV.ADDRESS_RED_BANK || !ENV.URL_GQL || !ENV.URL_API) {
return res.status(404).json(ENV_MISSING_MESSAGE)
}
const markets = await (await fetch(`${ENV.URL_API}/markets${VERCEL_BYPASS}`)).json()
export default async function getMarketDeposits(): Promise<Coin[]> {
const markets = await getMarkets()
let query = ''
@ -45,10 +41,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
amount: result.deposits[key],
}
})
return res.status(200).json(deposits)
return deposits
}
return res.status(404)
return new Promise((_, reject) => reject('No data'))
}
interface DepositsQuery {

View File

@ -0,0 +1,30 @@
import { BN } from 'utils/helpers'
import getMarketDeposits from 'api/markets/getMarketDeposits'
import getMarketDebts from 'api/markets/getMarketDebts'
export default async function getMarketLiquidity(): Promise<Coin[]> {
const deposits = await getMarketDeposits()
const debts = await getMarketDebts()
const liquidity: Coin[] = deposits.map((deposit) => {
const debt = debts.find((debt) => debt.denom === deposit.denom)
if (debt) {
return {
denom: deposit.denom,
amount: BN(deposit.amount).minus(debt.amount).toString(),
}
}
return {
denom: deposit.denom,
amount: '0',
}
})
if (liquidity) {
return liquidity
}
return new Promise((_, reject) => reject('No data'))
}

View File

@ -1,16 +1,11 @@
import { gql, request as gqlRequest } from 'graphql-request'
import { NextApiRequest, NextApiResponse } from 'next'
import { ENV, ENV_MISSING_MESSAGE } from 'constants/env'
import { ENV } from 'constants/env'
import { getMarketAssets } from 'utils/assets'
import { denomToKey } from 'utils/query'
import { resolveMarketResponses } from 'utils/resolvers'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!ENV.URL_GQL || !ENV.ADDRESS_RED_BANK || !ENV.ADDRESS_INCENTIVES) {
return res.status(404).json(ENV_MISSING_MESSAGE)
}
export default async function getMarkets(): Promise<Market[]> {
const marketAssets = getMarketAssets()
const marketQueries = marketAssets.map(
@ -36,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const market = result.rbwasmkey[`${denomToKey(asset.denom)}`]
return market
})
return res.status(200).json(resolveMarketResponses(markets))
return resolveMarketResponses(markets)
}
interface RedBankData {

View File

@ -1,16 +1,11 @@
import { gql, request as gqlRequest } from 'graphql-request'
import { NextApiRequest, NextApiResponse } from 'next'
import { ASSETS } from 'constants/assets'
import { ENV, ENV_MISSING_MESSAGE } from 'constants/env'
import { ENV } from 'constants/env'
import { getMarketAssets } from 'utils/assets'
import { BN } from 'utils/helpers'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!ENV.URL_GQL || !ENV.ADDRESS_ORACLE) {
return res.status(404).json(ENV_MISSING_MESSAGE)
}
export default async function getPrices(): Promise<Coin[]> {
const marketAssets = getMarketAssets()
const baseCurrency = ASSETS[0]
@ -51,7 +46,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
] as Coin[]
}, [])
return res.status(200).json(data)
return data
}
interface TokenPricesResult {

View File

@ -0,0 +1,50 @@
import { ENV } from 'constants/env'
export default async function getAprs() {
try {
const response = await fetch(ENV.URL_APOLLO_APR)
if (response.ok) {
const data: FlatApr[] | NestedApr[] = await response.json()
const newAprs = data.map((aprData) => {
try {
const apr = aprData as FlatApr
const aprTotal = apr.apr.reduce((prev, curr) => Number(curr.value) + prev, 0)
const feeTotal = apr.fees.reduce((prev, curr) => Number(curr.value) + prev, 0)
const finalApr = aprTotal + feeTotal
return { address: aprData.contract_address, apr: finalApr }
} catch {
const apr = aprData as NestedApr
const aprTotal = apr.apr.aprs.reduce((prev, curr) => Number(curr.value) + prev, 0)
const feeTotal = apr.apr.fees.reduce((prev, curr) => Number(curr.value) + prev, 0)
const finalApr = aprTotal + feeTotal
return { address: aprData.contract_address, apr: finalApr }
}
})
return newAprs
}
return []
} catch {
return []
}
}
interface FlatApr {
contract_address: string
apr: { type: string; value: number | string }[]
fees: { type: string; value: number | string }[]
}
interface NestedApr {
contract_address: string
apr: {
aprs: { type: string; value: number | string }[]
fees: { type: string; value: number | string }[]
}
}

View File

@ -0,0 +1,64 @@
import { getClient } from 'api/cosmwasm-client'
import { ENV, IS_TESTNET } from 'constants/env'
import { TESTNET_VAULTS, VAULTS } from 'constants/vaults'
import {
ArrayOfVaultInfoResponse,
VaultBaseForString,
} from 'types/generated/mars-credit-manager/MarsCreditManager.types'
export default async function getVaultConfigs(): Promise<VaultConfig[]> {
const vaultInfos: VaultInfo[] = await getVaultInfos([])
const vaults = IS_TESTNET ? TESTNET_VAULTS : VAULTS
return vaults.map((vaultMetaData) => {
const vaultConfig = vaultInfos.find((vaultInfo) => vaultInfo.address === vaultMetaData.address)
return {
...vaultMetaData,
...vaultConfig,
} as VaultConfig
})
}
const getVaultInfos = async (
vaultInfos: VaultInfo[],
startAfter?: VaultBaseForString,
): Promise<VaultInfo[]> => {
if (!ENV.ADDRESS_CREDIT_MANAGER) return []
const client = await getClient()
try {
const batch: ArrayOfVaultInfoResponse = await client.queryContractSmart(
ENV.ADDRESS_CREDIT_MANAGER,
{
vaults_info: { limit: 4, start_after: startAfter },
},
)
const batchProcessed = batch?.map((vaultInfo) => {
return {
address: vaultInfo.vault.address,
cap: {
denom: vaultInfo.config.deposit_cap.denom,
used: Number(vaultInfo.utilization.amount),
max: Number(vaultInfo.config.deposit_cap.amount),
},
ltv: {
max: Number(vaultInfo.config.max_ltv),
liq: Number(vaultInfo.config.liquidation_threshold),
},
} as VaultConfig
})
vaultInfos.push(...batchProcessed)
if (batch.length === 4) {
return await getVaultInfos(vaultInfos, {
address: batchProcessed[batchProcessed.length - 1].address,
} as VaultBaseForString)
}
return vaultInfos
} catch {
return vaultInfos
}
}

View File

@ -0,0 +1,29 @@
import { convertAprToApy } from 'utils/parsers'
import getVaultConfigs from 'api/vaults/getVaultConfigs'
import getAprs from 'api/vaults/getVaultAprs'
export default async function getVaults(): Promise<Vault[]> {
const $vaultConfigs = getVaultConfigs()
const $aprs = getAprs()
const vaults: Vault[] = await Promise.all([$vaultConfigs, $aprs]).then(([vaultConfigs, aprs]) => {
return vaultConfigs.map((vaultConfig) => {
const apr = aprs.find((apr) => apr.address === vaultConfig.address)
if (apr) {
return {
...vaultConfig,
apy: convertAprToApy(apr.apr, 365),
}
}
return {
...vaultConfig,
apy: null,
}
})
})
if (vaults) {
return vaults
}
return new Promise((_, reject) => reject('No data'))
}

View File

@ -0,0 +1,18 @@
import { getClient } from 'api/cosmwasm-client'
import { ENV } from 'constants/env'
export default async function getAccountIds(address: string) {
const client = await getClient()
const data = await client.queryContractSmart(ENV.ADDRESS_ACCOUNT_NFT, {
tokens: {
owner: address,
},
})
if (data.tokens) {
return data.tokens
}
return new Promise((_, reject) => reject('No data'))
}

View File

@ -0,0 +1,26 @@
import { ENV } from 'constants/env'
import { resolvePositionResponses } from 'utils/resolvers'
import getWalletAccountIds from 'api/wallets/getAccountIds'
import { getClient } from 'api/cosmwasm-client'
export default async function getAccounts(address: string): Promise<Account[]> {
const accountIds: string[] = await getWalletAccountIds(address)
const client = await getClient()
const $accounts: Promise<AccountResponse>[] = accountIds.map((accountId) =>
client.queryContractSmart(ENV.ADDRESS_CREDIT_MANAGER!, {
positions: {
account_id: `${accountId}`,
},
}),
)
const accounts = await Promise.all($accounts).then((accounts) => accounts)
if (accounts) {
return resolvePositionResponses(accounts)
}
return new Promise((_, reject) => reject('No data'))
}

View File

@ -0,0 +1,14 @@
import { ENV } from 'constants/env'
export default async function getWalletBalances(address: string): Promise<Coin[]> {
const uri = '/cosmos/bank/v1beta1/balances/'
const response = await fetch(`${ENV.URL_REST}${uri}${address}`)
if (response.ok) {
const data = await response.json()
return data.balances
}
return new Promise((_, reject) => reject('No data'))
}

View File

@ -1,5 +0,0 @@
import BorrowPage from 'components/pages/borrow'
export default async function page({ params }: PageProps) {
return <BorrowPage params={params} />
}

View File

@ -1,5 +0,0 @@
import CouncilPage from 'components/pages/council'
export default async function page({ params }: PageProps) {
return <CouncilPage params={params} />
}

View File

@ -1,5 +0,0 @@
import FarmPage from 'components/pages/farm'
export default async function page({ params }: { params: PageParams }) {
return <FarmPage params={params} />
}

View File

@ -1,5 +0,0 @@
import LendPage from 'components/pages/lend'
export default function page({ params }: { params: PageParams }) {
return <LendPage params={params} />
}

View File

@ -1,30 +0,0 @@
export default function Head() {
return (
<>
<title>Mars Protocol V2</title>
<meta charSet='utf-8' />
<link href='/favicon.svg' rel='icon' />
<link href='/apple-touch-icon.png' rel='apple-touch-icon' sizes='180x180' />
<link href='/site.webmanifest' rel='manifest' />
<link color='#dd5b65' href='/safari-pinned-tab.svg' rel='mask-icon' />
<meta content='index,follow' name='robots' />
<meta
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies."
name='description'
/>
<meta content='summary_large_image' name='twitter:card' />
<meta content='@mars_protocol' name='twitter:site' />
<meta content='@mars_protocol' name='twitter:creator' />
<meta content='https://osmosis.marsprotocol.io' property='og:url' />
<meta content='Mars Protocol V2 - Powered by Osmosis' property='og:title' />
<meta
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies."
property='og:description'
/>
<meta content='https://osmosis.marsprotocol.io/banner.png' property='og:image' />
<meta content='Mars Protocol V2' property='og:site_name' />
<meta content='#ffffff' name='msapplication-TileColor' />
<meta content='#ffffff' name='theme-color' />
</>
)
}

View File

@ -1,42 +0,0 @@
import classNames from 'classnames'
import { headers } from 'next/headers'
import AccountDetails from 'components/Account/AccountDetails'
import Background from 'components/Background'
import FetchPrices from 'components/FetchPrices'
import Footer from 'components/Footer'
import DesktopHeader from 'components/Header/DesktopHeader'
import ModalsContainer from 'components/Modals/ModalsContainer'
import Toaster from 'components/Toaster'
import 'react-toastify/dist/ReactToastify.min.css'
import 'styles/globals.css'
import { getRouteParams } from 'utils/route'
export default function RootLayout(props: { children: React.ReactNode }) {
const href = headers().get('x-url') || ''
const params = getRouteParams(href)
return (
<html className='m-0 p-0' lang='en'>
<head />
<body className='m-0 cursor-default bg-body p-0 font-sans text-white'>
<Background />
<DesktopHeader params={params} />
<FetchPrices />
<main
className={classNames(
'relative flex justify-center pt-6',
'lg:mt-[65px] lg:h-[calc(100vh-89px)]',
)}
>
<div className='flex w-full max-w-content flex-grow flex-wrap content-start'>
{props.children}
</div>
<AccountDetails />
</main>
<Footer />
<ModalsContainer />
<Toaster />
</body>
</html>
)
}

View File

@ -1,5 +0,0 @@
import TradePage from 'components/pages/trade'
export default async function page({ params }: PageProps) {
return <TradePage params={params} />
}

View File

@ -1,5 +0,0 @@
import PortfolioPage from 'components/pages/portfolio'
export default async function page({ params }: PageProps) {
return <PortfolioPage params={params} />
}

View File

@ -1,5 +0,0 @@
import TradePage from 'components/pages/trade'
export default async function page({ params }: PageProps) {
return <TradePage params={params} />
}

View File

@ -1,5 +0,0 @@
import BorrowPage from 'components/pages/borrow'
export default async function page({ params }: PageProps) {
return <BorrowPage params={params} />
}

View File

@ -1,5 +0,0 @@
import CouncilPage from 'components/pages/council'
export default async function page({ params }: PageProps) {
return <CouncilPage params={params} />
}

View File

@ -1,5 +0,0 @@
import FarmPage from 'components/pages/farm'
export default async function page({ params }: { params: PageParams }) {
return <FarmPage params={params} />
}

View File

@ -1,5 +0,0 @@
import LendPage from 'components/pages/lend'
export default function page({ params }: { params: PageParams }) {
return <LendPage params={params} />
}

View File

@ -1,5 +0,0 @@
import TradePage from 'components/pages/trade'
export default async function page({ params }: PageProps) {
return <TradePage params={params} />
}

View File

@ -1,5 +0,0 @@
import PortfolioPage from 'components/pages/portfolio'
export default async function page({ params }: PageProps) {
return <PortfolioPage params={params} />
}

View File

@ -1,5 +0,0 @@
import TradePage from 'components/pages/trade'
export default async function page({ params }: PageProps) {
return <TradePage params={params} />
}

View File

@ -1,5 +0,0 @@
import BorrowPage from 'components/pages/borrow'
export default async function page({ params }: PageProps) {
return <BorrowPage params={params} />
}

View File

@ -1,5 +0,0 @@
import CouncilPage from 'components/pages/council'
export default async function page({ params }: PageProps) {
return <CouncilPage params={params} />
}

View File

@ -1,5 +0,0 @@
import FarmPage from 'components/pages/farm'
export default async function page({ params }: { params: PageParams }) {
return <FarmPage params={params} />
}

View File

@ -1,5 +0,0 @@
import LendPage from 'components/pages/lend'
export default function page({ params }: { params: PageParams }) {
return <LendPage params={params} />
}

View File

@ -1,3 +0,0 @@
export default function RootLayout({ children }: { children: React.ReactNode }) {
return children
}

View File

@ -1,5 +0,0 @@
import PortfolioPage from 'components/pages/portfolio'
export default async function page({ params }: PageProps) {
return <PortfolioPage params={params} />
}

View File

@ -1,5 +0,0 @@
import TradePage from 'components/pages/trade'
export default async function page({ params }: PageProps) {
return <TradePage params={params} />
}

View File

@ -1,5 +1,3 @@
'use client'
import {
ColumnDef,
flexRender,
@ -142,7 +140,6 @@ export const AcccountBalancesTable = (props: Props) => {
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
debugTable: true,
})
return (

View File

@ -1,4 +1,3 @@
'use client'
import BigNumber from 'bignumber.js'
import classNames from 'classnames'

View File

@ -1,13 +1,13 @@
'use client'
import { useParams } from 'react-router-dom'
import { Gauge } from 'components/Gauge'
import { Heart } from 'components/Icons'
import Text from 'components/Text'
import { isNumber } from 'utils/parsers'
import useParams from 'utils/route'
export default function AccountDetails() {
const params = useParams()
const hasAccount = isNumber(params.accountId)
const { accountId } = useParams()
const hasAccount = isNumber(accountId)
return hasAccount ? (
<div

View File

@ -1,4 +1,3 @@
'use client'
import classNames from 'classnames'
import { Heart } from 'components/Icons'

View File

@ -1,8 +1,6 @@
'use client'
import classNames from 'classnames'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import AccountStats from 'components/Account/AccountStats'
import { Button } from 'components/Button'
@ -16,7 +14,8 @@ import useStore from 'store'
import { calculateAccountDeposits } from 'utils/accounts'
import { hardcodedFee } from 'utils/contants'
import { BN } from 'utils/helpers'
import useParams, { getRoute } from 'utils/route'
import { getPage, getRoute } from 'utils/route'
import usePrices from 'hooks/usePrices'
interface Props {
setShowFundAccount: (showFundAccount: boolean) => void
@ -29,26 +28,25 @@ const accountCardHeaderClasses = classNames(
)
export default function AccountList(props: Props) {
const router = useRouter()
const params = useParams()
const selectedAccount = params.accountId
const prices = useStore((s) => s.prices)
const navigate = useNavigate()
const { pathname } = useLocation()
const { accountId, address } = useParams()
const { data: prices } = usePrices()
const deleteAccount = useStore((s) => s.deleteAccount)
const [isLending, setIsLending] = useToggle()
const accountSelected = !!selectedAccount && !isNaN(Number(selectedAccount))
const selectedAccountDetails = props.accounts.find((account) => account.id === selectedAccount)
const accountSelected = !!accountId && !isNaN(Number(accountId))
const selectedAccountDetails = props.accounts.find((account) => account.id === accountId)
const selectedAccountBalance = selectedAccountDetails
? calculateAccountDeposits(selectedAccountDetails, prices)
: BN(0)
async function deleteAccountHandler() {
if (!accountSelected) return
const isSuccess = await deleteAccount({ fee: hardcodedFee, accountId: selectedAccount })
const isSuccess = await deleteAccount({ fee: hardcodedFee, accountId: accountId })
if (isSuccess) {
router.push(`/wallets/${params.address}/accounts`)
navigate(`/wallets/${address}/accounts`)
}
}
@ -58,11 +56,11 @@ export default function AccountList(props: Props) {
}
useEffect(() => {
const element = document.getElementById(`account-${selectedAccount}`)
const element = document.getElementById(`account-${accountId}`)
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
}, [selectedAccount])
}, [accountId])
if (!props.accounts?.length) return null
@ -70,7 +68,7 @@ export default function AccountList(props: Props) {
<div className='flex w-full flex-wrap p-4'>
{props.accounts.map((account) => {
const positionBalance = calculateAccountDeposits(account, prices)
const isActive = selectedAccount === account.id
const isActive = accountId === account.id
return (
<div key={account.id} id={`account-${account.id}`} className='w-full pt-4'>
<Card
@ -87,7 +85,7 @@ export default function AccountList(props: Props) {
role={!isActive ? 'button' : undefined}
onClick={() => {
if (isActive) return
router.push(getRoute(params, { accountId: account.id }))
navigate(getRoute(getPage(pathname), address, account.id))
}}
>
<Text size='xs' className='flex flex-1'>

View File

@ -2,23 +2,20 @@ import { Suspense } from 'react'
import AccountMenuContent from 'components/Account/AccountMenuContent'
import Loading from 'components/Loading'
import { getAccounts } from 'utils/api'
import useStore from 'store'
import useAccounts from 'hooks/useAccounts'
interface Props {
params: PageParams
}
async function Content(props: Props) {
if (props.params.address === undefined) return null
const accounts = await getAccounts(props.params.address)
function Content() {
const address = useStore((s) => s.address)
const { data: accounts } = useAccounts(address)
if (!accounts) return null
return <AccountMenuContent accounts={accounts} />
}
export default function AccountMenu(props: Props) {
export default function AccountMenu() {
return (
<Suspense fallback={<Loading className='h-8 w-35' />}>
{/* @ts-expect-error Server Component */}
<Content params={props.params} />
<Content />
</Suspense>
)
}

View File

@ -1,8 +1,6 @@
'use client'
import classNames from 'classnames'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import AccountList from 'components/Account/AccountList'
import CreateAccount from 'components/Account/CreateAccount'
@ -16,7 +14,6 @@ import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { hardcodedFee } from 'utils/contants'
import { isNumber } from 'utils/parsers'
import useParams from 'utils/route'
const menuClasses = 'absolute isolate flex w-full flex-wrap scrollbar-hide'
@ -25,22 +22,21 @@ interface Props {
}
export default function AccountMenuContent(props: Props) {
const router = useRouter()
const params = useParams()
const navigate = useNavigate()
const { accountId, address } = useParams()
const createAccount = useStore((s) => s.createAccount)
const [showMenu, setShowMenu] = useToggle()
const [isCreating, setIsCreating] = useToggle()
const selectedAccountId = params.accountId
const hasCreditAccounts = !!props.accounts.length
const isAccountSelected = isNumber(selectedAccountId)
const isAccountSelected = isNumber(accountId)
const selectedAccountDetails = props.accounts.find((account) => account.id === selectedAccountId)
const selectedAccountDetails = props.accounts.find((account) => account.id === accountId)
const [showFundAccount, setShowFundAccount] = useState<boolean>(
isAccountSelected && !selectedAccountDetails?.deposits?.length,
)
const isLoadingAccount = isAccountSelected && selectedAccountDetails?.id !== selectedAccountId
const isLoadingAccount = isAccountSelected && selectedAccountDetails?.id !== accountId
const showCreateAccount = !hasCreditAccounts || isCreating
async function createAccountHandler() {
@ -49,14 +45,14 @@ export default function AccountMenuContent(props: Props) {
const accountId = await createAccount({ fee: hardcodedFee })
setIsCreating(false)
if (!accountId) return
router.push(`/wallets/${params.address}/accounts/${accountId}`)
navigate(`/wallets/${address}/accounts/${accountId}`)
}
useEffect(() => {
useStore.setState({ accounts: props.accounts })
}, [props.accounts])
if (!params.address) return null
if (!address) return null
return (
<div className='relative'>
@ -69,7 +65,7 @@ export default function AccountMenuContent(props: Props) {
>
{hasCreditAccounts
? isAccountSelected
? `Account ${selectedAccountId}`
? `Account ${accountId}`
: 'Select Account'
: 'Create Account'}
</Button>

View File

@ -1,4 +1,3 @@
'use client'
import BigNumber from 'bignumber.js'
import AccountHealth from 'components/Account/AccountHealth'

View File

@ -1,5 +1,3 @@
'use client'
import Accordion from 'components/Accordion'
import { AcccountBalancesTable } from 'components/Account/AccountBalancesTable'
import AccountComposition from 'components/Account/AccountComposition'

View File

@ -1,5 +1,3 @@
'use client'
import { Button } from 'components/Button'
import { ArrowRight } from 'components/Icons'
import Text from 'components/Text'

View File

@ -1,7 +1,6 @@
'use client'
import BigNumber from 'bignumber.js'
import { useCallback, useState } from 'react'
import { useParams } from 'react-router-dom'
import { Button } from 'components/Button'
import { ArrowRight, Cross } from 'components/Icons'
@ -14,7 +13,6 @@ import useStore from 'store'
import { getAmount } from 'utils/accounts'
import { hardcodedFee } from 'utils/contants'
import { BN } from 'utils/helpers'
import useParams from 'utils/route'
interface Props {
setShowFundAccount: (show: boolean) => void
@ -22,7 +20,7 @@ interface Props {
}
export default function FundAccount(props: Props) {
const params = useParams()
const { accountId } = useParams()
const deposit = useStore((s) => s.deposit)
const balances = useStore((s) => s.balances)
@ -50,10 +48,11 @@ export default function FundAccount(props: Props) {
)
async function onDeposit() {
if (!accountId) return
setIsFunding(true)
const result = await deposit({
fee: hardcodedFee,
accountId: params.accountId,
accountId,
coin: {
denom: asset.denom,
amount: amount.toString(),
@ -79,7 +78,7 @@ export default function FundAccount(props: Props) {
</div>
<div className='relative z-10 w-full p-4'>
<Text size='lg' className='mb-2 font-bold'>
{`Fund Account ${params.accountId}`}
{`Fund Account ${accountId}`}
</Text>
<Text className='mb-4 text-white/70'>
Deposit assets from your Osmosis address to your Mars credit account. Bridge assets if

View File

@ -1,4 +1,4 @@
import { getAccountDebts } from 'utils/api'
import getAccountDebts from 'api/accounts/getAccountDebts'
interface Props {
accountId: string

View File

@ -1,5 +1,3 @@
'use client'
import classNames from 'classnames'
import useStore from 'store'

View File

@ -1,5 +1,3 @@
'use client'
import { Row } from '@tanstack/react-table'
import { Button } from 'components/Button'

View File

View File

@ -1,5 +1,3 @@
'use client'
import {
ColumnDef,
flexRender,
@ -124,7 +122,6 @@ export const BorrowTable = (props: Props) => {
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
debugTable: true,
})
return (

View File

@ -1,17 +1,20 @@
import { Suspense } from 'react'
import { useParams } from 'react-router-dom'
import Card from 'components/Card'
import { getAccountDebts, getBorrowData } from 'utils/api'
import { getMarketAssets } from 'utils/assets'
import { BorrowTable } from 'components/Borrow/BorrowTable'
import useAccountDebts from 'hooks/useAccountDebts'
import useMarketBorrowings from 'hooks/useMarketBorrowings'
interface Props extends PageProps {
interface Props {
type: 'active' | 'available'
}
async function Content(props: Props) {
const debtData = await getAccountDebts(props.params?.accountId)
const borrowData = await getBorrowData()
function Content(props: Props) {
const { accountId } = useParams()
const { data: debtData } = useAccountDebts(accountId)
const { data: borrowData } = useMarketBorrowings()
const marketAssets = getMarketAssets()
@ -20,7 +23,7 @@ async function Content(props: Props) {
(prev: { available: BorrowAsset[]; active: BorrowAssetActive[] }, curr) => {
const borrow = borrowData.find((borrow) => borrow.denom === curr.denom)
if (borrow) {
const debt = debtData.find((debt) => debt.denom === curr.denom)
const debt = debtData?.find((debt) => debt.denom === curr.denom)
if (debt) {
prev.active.push({
...borrow,
@ -68,22 +71,20 @@ function Fallback() {
return <BorrowTable data={available} />
}
export function AvailableBorrowings(props: PageProps) {
export function AvailableBorrowings() {
return (
<Card className='h-fit w-full bg-white/5' title={'Available to borrow'}>
<Suspense fallback={<Fallback />}>
{/* @ts-expect-error Server Component */}
<Content params={props.params} type='available' />
<Content type='available' />
</Suspense>
</Card>
)
}
export function ActiveBorrowings(props: PageProps) {
export function ActiveBorrowings() {
return (
<Suspense fallback={null}>
{/* @ts-expect-error Server Component */}
<Content params={props.params} type='active' />
<Content type='active' />
</Suspense>
)
}

View File

@ -1,11 +1,12 @@
import { Suspense } from 'react'
import { useParams } from 'react-router-dom'
import Card from 'components/Card'
import Loading from 'components/Loading'
import Text from 'components/Text'
async function Content(props: PageProps) {
const address = props.params.address
function Content() {
const address = useParams().address || ''
return address ? (
<Text size='sm'>{`Council page for ${address}`}</Text>
@ -18,7 +19,7 @@ function Fallback() {
return <Loading className='h-4 w-50' />
}
export default function Overview(props: PageProps) {
export default function Overview() {
return (
<Card
className='h-fit w-full justify-center bg-white/5'
@ -26,8 +27,7 @@ export default function Overview(props: PageProps) {
contentClassName='px-4 py-6'
>
<Suspense fallback={<Fallback />}>
{/* @ts-expect-error Server Component */}
<Content params={props.params} />
<Content />
</Suspense>
</Card>
)

View File

@ -1,5 +1,3 @@
import { Coin } from '@cosmjs/stargate'
import { FormattedNumber } from 'components/FormattedNumber'
import useStore from 'store'
import { convertToDisplayAmount } from 'utils/formatters'

View File

@ -1,5 +1,5 @@
import classNames from 'classnames'
import Link from 'next/link'
import { NavLink, useParams } from 'react-router-dom'
import { getRoute } from 'utils/route'
@ -7,34 +7,35 @@ const underlineClasses =
'relative before:absolute before:h-[2px] before:-bottom-1 before:left-0 before:right-0 before:gradient-active-tab'
interface Props {
params: PageParams
isFarm?: boolean
}
export default function Tab(props: Props) {
const { address, accountId } = useParams()
return (
<div className='mb-8 w-full'>
<div className='flex gap-2'>
<div className='relative'>
<Link
href={getRoute(props.params, { page: 'earn/farm' })}
<NavLink
to={getRoute('farm', address, accountId)}
className={classNames(
!props.isFarm ? 'text-white/20' : underlineClasses,
'relative mr-8 text-xl',
)}
>
Farm
</Link>
</NavLink>
</div>
<Link
href={getRoute(props.params, { page: 'earn/lend' })}
<NavLink
to={getRoute('lend', address, accountId)}
className={classNames(
props.isFarm ? 'text-white/20' : underlineClasses,
'relative text-xl',
)}
>
Lend
</Link>
</NavLink>
</div>
</div>
)

View File

@ -5,10 +5,10 @@ import { VaultTable } from 'components/Earn/vault/VaultTable'
import Text from 'components/Text'
import { IS_TESTNET } from 'constants/env'
import { TESTNET_VAULTS, VAULTS } from 'constants/vaults'
import { getVaults } from 'utils/api'
import useVaults from 'hooks/useVaults'
async function Content() {
const vaults = await getVaults()
function Content() {
const { data: vaults } = useVaults()
if (!vaults.length) return null
@ -19,7 +19,6 @@ export default function AvailableVaults() {
return (
<Card title='Available vaults' className='mb-4 h-fit w-full bg-white/5'>
<Suspense fallback={<Fallback />}>
{/* @ts-expect-error Server Component */}
<Content />
</Suspense>
</Card>

View File

@ -2,10 +2,10 @@ import { Suspense } from 'react'
import Card from 'components/Card'
import VaultCard from 'components/Earn/vault/VaultCard'
import { getVaults } from 'utils/api'
import useVaults from 'hooks/useVaults'
async function Content() {
const vaults = await getVaults()
function Content() {
const { data: vaults } = useVaults()
const featuredVaults = vaults.filter((vault) => vault.isFeatured)
@ -33,7 +33,6 @@ async function Content() {
export default function FeaturedVaults() {
return (
<Suspense fallback={null}>
{/* @ts-expect-error Server Component */}
<Content />
</Suspense>
)

View File

@ -1,5 +1,3 @@
'use client'
import { Button } from 'components/Button'
import VaultLogo from 'components/Earn/vault/VaultLogo'
import Text from 'components/Text'

View File

@ -1,5 +1,3 @@
'use client'
import {
ColumnDef,
flexRender,
@ -103,7 +101,6 @@ export const VaultTable = (props: Props) => {
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
debugTable: true,
})
return (

View File

@ -1,5 +1,3 @@
'use client'
import classNames from 'classnames'
import React, { useEffect, useRef } from 'react'
import { animated, useSpring } from 'react-spring'

View File

@ -4,21 +4,18 @@ import AccountMenu from 'components/Account/AccountMenu'
import DesktopNavigation from 'components/Navigation/DesktopNavigation'
import Settings from 'components/Settings'
import Wallet from 'components/Wallet/Wallet'
import { WalletConnectProvider } from 'components/Wallet/WalletConnectProvider'
import useStore from 'store'
export const menuTree: { href: RouteSegment; label: string }[] = [
{ href: 'trade', label: 'Trade' },
{ href: 'earn/farm', label: 'Earn' },
{ href: 'borrow', label: 'Borrow' },
{ href: 'portfolio', label: 'Portfolio' },
{ href: 'council', label: 'Council' },
export const menuTree: { page: Page; label: string }[] = [
{ page: 'trade', label: 'Trade' },
{ page: 'farm', label: 'Earn' },
{ page: 'borrow', label: 'Borrow' },
{ page: 'portfolio', label: 'Portfolio' },
{ page: 'council', label: 'Council' },
]
interface Props {
params: PageParams
}
export default function DesktopHeader(props: Props) {
export default function DesktopHeader() {
const address = useStore((s) => s.address)
return (
<header
className={classNames(
@ -30,10 +27,8 @@ export default function DesktopHeader(props: Props) {
<div className='flex items-center justify-between border-b border-white/20 py-3 pl-6 pr-4'>
<DesktopNavigation />
<div className='flex gap-4'>
<AccountMenu params={props.params} />
<WalletConnectProvider>
<Wallet />
</WalletConnectProvider>
{address && <AccountMenu />}
<Wallet />
<Settings />
</div>
</div>

View File

@ -1,5 +1,3 @@
'use client'
import classNames from 'classnames'
import { ReactNode, useEffect, useRef } from 'react'

View File

@ -1,5 +1,3 @@
'use client'
import Image from 'next/image'
import { useEffect, useState } from 'react'

View File

@ -1,5 +1,3 @@
'use client'
import { useEffect, useState } from 'react'
import AccountSummary from 'components/Account/AccountSummary'

View File

@ -1,5 +1,3 @@
'use client'
import BorrowModal from 'components/Modals/BorrowModal'
import FundAndWithdrawModal from 'components/Modals/FundAndWithdrawModal'
import VaultModal from 'components/Modals/VaultModal'

View File

@ -1,33 +1,30 @@
'use client'
import Link from 'next/link'
import { useParams } from 'react-router-dom'
import { menuTree } from 'components/Header/DesktopHeader'
import { Logo } from 'components/Icons'
import { NavLink } from 'components/Navigation/NavLink'
import useParams, { getRoute } from 'utils/route'
import { getRoute } from 'utils/route'
export default function DesktopNavigation() {
const params = useParams()
const { address, accountId } = useParams()
function getIsActive(href: string) {
if (params.page.includes('earn') && href.includes('earn')) return true
return params.page === href
return location.pathname.includes(href)
}
return (
<div className='flex flex-grow items-center'>
<Link href={getRoute(params, { page: 'trade' })}>
<NavLink href={getRoute('trade', address, accountId)} isActive={false}>
<span className='block h-10 w-10'>
<Logo />
</span>
</Link>
</NavLink>
<div className='flex gap-8 px-6'>
{menuTree.map((item, index) => (
<NavLink
key={index}
href={getRoute(params, { page: item.href })}
isActive={getIsActive(item.href)}
href={getRoute(item.page, address, accountId)}
isActive={getIsActive(item.page)}
>
{item.label}
</NavLink>

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'
import Link from 'next/link'
import { ReactNode } from 'react'
import { NavLink as Link } from 'react-router-dom'
interface Props {
href: string
@ -11,11 +11,13 @@ interface Props {
export const NavLink = (props: Props) => {
return (
<Link
href={props.href}
className={classNames(
'text-sm font-semibold hover:text-white active:text-white',
props.isActive ? 'pointer-events-none text-white' : 'text-white/60',
)}
to={props.href}
className={({ isActive }) =>
classNames(
'text-sm font-semibold hover:text-white active:text-white',
isActive ? 'pointer-events-none text-white' : 'text-white/60',
)
}
>
{props.children}
</Link>

View File

@ -1,5 +1,3 @@
'use client'
import BigNumber from 'bignumber.js'
import classNames from 'classnames'
import React, { useEffect, useState } from 'react'

View File

@ -1,16 +1,17 @@
import classNames from 'classnames'
import { Suspense } from 'react'
import { useParams } from 'react-router-dom'
import { AcccountBalancesTable } from 'components/Account/AccountBalancesTable'
import AccountComposition from 'components/Account/AccountComposition'
import Card from 'components/Card'
import Loading from 'components/Loading'
import Text from 'components/Text'
import { getAccounts } from 'utils/api'
import useAccounts from 'hooks/useAccounts'
async function Content(props: PageProps) {
const address = props.params.address
const account = await getAccounts(address)
function Content() {
const address = useParams().address || ''
const { data: account } = useAccounts(address)
if (!address) {
return (
@ -60,11 +61,10 @@ function Fallback() {
)
}
export default function AccountOverview(props: PageProps) {
export default function AccountOverview() {
return (
<Suspense fallback={<Fallback />}>
{/* @ts-expect-error Server Component */}
<Content params={props.params} />
<Content />
</Suspense>
)
}

49
src/components/Routes.tsx Normal file
View File

@ -0,0 +1,49 @@
import { Outlet, Route, Routes as RoutesWrapper } from 'react-router-dom'
import BorrowPage from 'pages/BorrowPage'
import CouncilPage from 'pages/CouncilPage'
import FarmPage from 'pages/FarmPage'
import LendPage from 'pages/LendPage'
import PortfolioPage from 'pages/PortfolioPage'
import TradePage from 'pages/TradePage'
import Layout from 'pages/_layout'
export default function Routes() {
return (
<RoutesWrapper>
<Route
element={
<Layout>
<Outlet />
</Layout>
}
>
<Route path='/trade' element={<TradePage />} />
<Route path='/farm' element={<FarmPage />} />
<Route path='/lend' element={<LendPage />} />
<Route path='/borrow' element={<BorrowPage />} />
<Route path='/portfolio' element={<PortfolioPage />} />
<Route path='/council' element={<CouncilPage />} />
<Route path='/' element={<TradePage />} />
<Route path='/wallets/:address'>
<Route path='accounts/:accountId'>
<Route path='trade' element={<TradePage />} />
<Route path='farm' element={<FarmPage />} />
<Route path='lend' element={<LendPage />} />
<Route path='borrow' element={<BorrowPage />} />
<Route path='portfolio' element={<PortfolioPage />} />
<Route path='council' element={<CouncilPage />} />
<Route path='' element={<TradePage />} />
</Route>
<Route path='trade' element={<TradePage />} />
<Route path='farm' element={<FarmPage />} />
<Route path='lend' element={<LendPage />} />
<Route path='borrow' element={<BorrowPage />} />
<Route path='portfolio' element={<PortfolioPage />} />
<Route path='council' element={<CouncilPage />} />
<Route path='' element={<TradePage />} />
</Route>
</Route>
</RoutesWrapper>
)
}

View File

@ -1,5 +1,3 @@
'use client'
import classNames from 'classnames'
import { useEffect, useState } from 'react'

View File

@ -1,5 +1,3 @@
'use client'
import { Button } from 'components/Button'
import { Gear } from 'components/Icons'
import Overlay from 'components/Overlay'

View File

@ -1,5 +1,3 @@
'use client'
import classNames from 'classnames'
import { ChangeEvent, useRef, useState } from 'react'
import Draggable from 'react-draggable'

View File

@ -1,7 +1,7 @@
'use client'
import classNames from 'classnames'
import { useRouter } from 'next/navigation'
import { toast as createToast, Slide, ToastContainer } from 'react-toastify'
import { useNavigate } from 'react-router-dom'
import { mutate } from 'swr'
import { Button } from 'components/Button'
import { CheckCircled, Cross, CrossCircled } from 'components/Icons'
@ -11,7 +11,6 @@ import useStore from 'store'
export default function Toaster() {
const enableAnimations = useStore((s) => s.enableAnimations)
const toast = useStore((s) => s.toast)
const router = useRouter()
if (toast) {
const Msg = () => (
@ -62,7 +61,7 @@ export default function Toaster() {
})
useStore.setState({ toast: null })
router.refresh()
mutate(() => true)
}
return (

View File

@ -1,5 +1,3 @@
'use client'
import BigNumber from 'bignumber.js'
import classNames from 'classnames'
import Image from 'next/image'

View File

@ -1,5 +1,3 @@
'use client'
import BigNumber from 'bignumber.js'
import { useCallback, useEffect, useState } from 'react'

View File

@ -1,11 +1,13 @@
import { Suspense } from 'react'
import { useParams } from 'react-router-dom'
import Card from 'components/Card'
import Loading from 'components/Loading'
import Text from 'components/Text'
async function Content(props: PageProps) {
const address = props.params.address
function Content() {
const params = useParams()
const address = params.address
return address ? (
<Text size='sm'>{`Order book for ${address}`}</Text>
@ -20,12 +22,11 @@ function Fallback() {
return <Loading className='h-4 w-50' />
}
export default function OrderBook(props: PageProps) {
export default function OrderBook() {
return (
<Card className='col-span-3 bg-white/5' title='Order Book' contentClassName='px-4 py-6'>
<Suspense fallback={<Fallback />}>
{/* @ts-expect-error Server Component */}
<Content params={props.params} />
<Content />
</Suspense>
</Card>
)

View File

@ -1,12 +1,14 @@
import { Suspense } from 'react'
import { useParams } from 'react-router-dom'
import Card from 'components/Card'
import Loading from 'components/Loading'
import Text from 'components/Text'
async function Content(props: PageProps) {
const address = props.params.address
const currentAccount = props.params.accountId
function Content() {
const params = useParams()
const address = params.address
const currentAccount = params.accountId
const hasAccount = !isNaN(Number(currentAccount))
if (!address) return <Text size='sm'>You need to be connected to trade</Text>
@ -20,12 +22,11 @@ function Fallback() {
return <Loading className='h-4 w-50' />
}
export default function Trade(props: PageProps) {
export default function Trade() {
return (
<Card className='h-full w-full bg-white/5' title='Trade Module' contentClassName='px-4 py-6'>
<Suspense fallback={<Fallback />}>
{/* @ts-expect-error Server Component */}
<Content params={props.params} />
<Content />
</Suspense>
</Card>
)

View File

@ -4,7 +4,7 @@ import Card from 'components/Card'
import Loading from 'components/Loading'
import Text from 'components/Text'
async function Content(props: PageProps) {
function Content() {
return <Text size='sm'>Chart view</Text>
}
@ -12,7 +12,7 @@ function Fallback() {
return <Loading className='h-4 w-50' />
}
export default function TradingView(props: PageProps) {
export default function TradingView() {
return (
<Card
className='col-span-2 h-full bg-white/5'
@ -20,8 +20,7 @@ export default function TradingView(props: PageProps) {
contentClassName='px-4 py-6'
>
<Suspense fallback={<Fallback />}>
{/* @ts-expect-error Server Component */}
<Content params={props.params} />
<Content />
</Suspense>
</Card>
)

View File

@ -1,5 +1,3 @@
'use client'
import { useWalletManager, WalletConnectionStatus } from '@marsprotocol/wallet-connector'
import { ReactNode } from 'react'

View File

@ -1,5 +1,3 @@
'use client'
import {
ChainInfoID,
SimpleChainInfoList,
@ -10,7 +8,6 @@ import BigNumber from 'bignumber.js'
import classNames from 'classnames'
import { useCallback, useEffect, useState } from 'react'
import useClipboard from 'react-use-clipboard'
import useSWR from 'swr'
import { Button } from 'components/Button'
import { CircularProgress } from 'components/CircularProgress'
@ -21,9 +18,9 @@ import Text from 'components/Text'
import { IS_TESTNET } from 'constants/env'
import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { Endpoints, getEndpoint, getWalletBalancesSWR } from 'utils/api'
import { getBaseAsset, getMarketAssets } from 'utils/assets'
import { formatValue, truncate } from 'utils/formatters'
import useWalletBalances from 'hooks/useWalletBalances'
export default function ConnectedButton() {
// ---------------
@ -35,10 +32,7 @@ export default function ConnectedButton() {
const address = useStore((s) => s.address)
const network = useStore((s) => s.client?.recentWallet.network)
const baseAsset = getBaseAsset()
const { data, isLoading } = useSWR(
getEndpoint(Endpoints.WALLET_BALANCES, { address }),
getWalletBalancesSWR,
)
const { data: walletBalances, isLoading } = useWalletBalances(address)
// ---------------
// LOCAL STATE
@ -66,17 +60,17 @@ export default function ConnectedButton() {
}
useEffect(() => {
if (!data || data.length === 0) return
if (!walletBalances || walletBalances.length === 0) return
setWalletAmount(
BigNumber(data?.find((coin: Coin) => coin.denom === baseAsset.denom)?.amount ?? 0)
BigNumber(walletBalances?.find((coin: Coin) => coin.denom === baseAsset.denom)?.amount ?? 0)
.div(10 ** baseAsset.decimals)
.toNumber(),
)
const assetDenoms = marketAssets.map((asset) => asset.denom)
const balances = data.filter((coin) => assetDenoms.includes(coin.denom))
const balances = walletBalances.filter((coin) => assetDenoms.includes(coin.denom))
useStore.setState({ balances })
}, [data, baseAsset.denom, baseAsset.decimals, marketAssets])
}, [walletBalances, baseAsset.denom, baseAsset.decimals, marketAssets])
return (
<div className={'relative'}>

View File

@ -1,28 +1,28 @@
'use client'
import {
getClient,
useWallet,
useWalletManager,
WalletConnectionStatus,
} from '@marsprotocol/wallet-connector'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import ConnectButton from 'components/Wallet/ConnectButton'
import ConnectedButton from 'components/Wallet/ConnectedButton'
import useParams from 'utils/route'
import useStore from 'store'
import { getPage, getRoute } from 'utils/route'
export default function Wallet() {
const router = useRouter()
const params = useParams()
const navigate = useNavigate()
const { address: addressInUrl } = useParams()
const { pathname } = useLocation()
const { status } = useWalletManager()
const { recentWallet, simulate, sign, broadcast } = useWallet()
const client = useStore((s) => s.client)
const address = useStore((s) => s.address)
// Set connection status
useEffect(() => {
const isConnected = status === WalletConnectionStatus.Connected
@ -34,29 +34,33 @@ export default function Wallet() {
}
: { address: undefined, accounts: null, client: undefined },
)
}, [status, recentWallet?.account.address])
if (!isConnected || !recentWallet) return
// Set the client
useEffect(() => {
if (!recentWallet || client) return
async function getCosmWasmClient() {
if (!recentWallet) return
if (!client) {
const getCosmWasmClient = async () => {
const cosmClient = await getClient(recentWallet.network.rpc)
const client = {
broadcast,
cosmWasmClient: cosmClient,
recentWallet,
sign,
simulate,
}
useStore.setState({ client })
const cosmClient = await getClient(recentWallet.network.rpc)
const client = {
broadcast,
cosmWasmClient: cosmClient,
recentWallet,
sign,
simulate,
}
getCosmWasmClient()
useStore.setState({ client })
}
if (!address || address === params.address) return
router.push(`/wallets/${address}`)
}, [address, broadcast, client, params, recentWallet, router, simulate, sign, status])
getCosmWasmClient()
}, [recentWallet, client, simulate, sign, broadcast])
// Redirect when switching wallets or on first connection
useEffect(() => {
if (!address || address === addressInUrl) return
navigate(getRoute(getPage(pathname), address))
}, [address, addressInUrl, navigate, pathname])
return address ? <ConnectedButton /> : <ConnectButton status={status} />
}

View File

@ -1,23 +1,16 @@
'use client'
import { ChainInfoID, WalletManagerProvider } from '@marsprotocol/wallet-connector'
import { FC } from 'react'
import { Button } from 'components/Button'
import { CircularProgress } from 'components/CircularProgress'
import { Cross } from 'components/Icons'
import { ENV, ENV_MISSING_MESSAGE } from 'constants/env'
import { ENV } from 'constants/env'
type Props = {
children?: React.ReactNode
}
export const WalletConnectProvider: FC<Props> = ({ children }) => {
if (!ENV.CHAIN_ID || !ENV.URL_REST || !ENV.URL_RPC || !ENV.WALLETS) {
console.error(ENV_MISSING_MESSAGE)
return null
}
const chainInfoOverrides = {
rpc: ENV.URL_RPC,
rest: ENV.URL_REST,

View File

@ -1,9 +0,0 @@
import Overview from 'components/Council/Overview'
interface Props {
params: PageParams
}
export default function Councilpage(props: Props) {
return <Overview params={props.params} />
}

View File

@ -1,9 +0,0 @@
import AccountOverview from 'components/Portfolio/AccountOverview'
interface Props {
params: PageParams
}
export default function Portfoliopage(props: Props) {
return <AccountOverview params={props.params} />
}

View File

@ -1,37 +1,39 @@
interface EnvironmentVariables {
ADDRESS_ACCOUNT_NFT: string | undefined
ADDRESS_CREDIT_MANAGER: string | undefined
ADDRESS_INCENTIVES: string | undefined
ADDRESS_ORACLE: string | undefined
ADDRESS_RED_BANK: string | undefined
ADDRESS_SWAPPER: string | undefined
ADDRESS_ZAPPER: string | undefined
CHAIN_ID: string | undefined
NETWORK: string | undefined
URL_GQL: string | undefined
URL_REST: string | undefined
URL_RPC: string | undefined
URL_API: string | undefined
WALLETS: string[] | undefined
ADDRESS_ACCOUNT_NFT: string
ADDRESS_CREDIT_MANAGER: string
ADDRESS_INCENTIVES: string
ADDRESS_ORACLE: string
ADDRESS_RED_BANK: string
ADDRESS_SWAPPER: string
ADDRESS_ZAPPER: string
CHAIN_ID: string
NETWORK: string
URL_GQL: string
URL_REST: string
URL_RPC: string
URL_API: string
URL_APOLLO_APR: string
WALLETS: string[]
}
export const ENV: EnvironmentVariables = {
ADDRESS_ACCOUNT_NFT: process.env.NEXT_PUBLIC_ACCOUNT_NFT,
ADDRESS_CREDIT_MANAGER: process.env.NEXT_PUBLIC_CREDIT_MANAGER,
ADDRESS_INCENTIVES: process.env.NEXT_PUBLIC_INCENTIVES,
ADDRESS_ORACLE: process.env.NEXT_PUBLIC_ORACLE,
ADDRESS_RED_BANK: process.env.NEXT_PUBLIC_RED_BANK,
ADDRESS_SWAPPER: process.env.NEXT_PUBLIC_SWAPPER,
ADDRESS_ZAPPER: process.env.NEXT_PUBLIC_ZAPPER,
CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID,
NETWORK: process.env.NEXT_PUBLIC_NETWORK,
URL_GQL: process.env.NEXT_PUBLIC_GQL,
URL_REST: process.env.NEXT_PUBLIC_REST,
URL_RPC: process.env.NEXT_PUBLIC_RPC,
ADDRESS_ACCOUNT_NFT: process.env.NEXT_PUBLIC_ACCOUNT_NFT || '',
ADDRESS_CREDIT_MANAGER: process.env.NEXT_PUBLIC_CREDIT_MANAGER || '',
ADDRESS_INCENTIVES: process.env.NEXT_PUBLIC_INCENTIVES || '',
ADDRESS_ORACLE: process.env.NEXT_PUBLIC_ORACLE || '',
ADDRESS_RED_BANK: process.env.NEXT_PUBLIC_RED_BANK || '',
ADDRESS_SWAPPER: process.env.NEXT_PUBLIC_SWAPPER || '',
ADDRESS_ZAPPER: process.env.NEXT_PUBLIC_ZAPPER || '',
CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID || '',
NETWORK: process.env.NEXT_PUBLIC_NETWORK || '',
URL_GQL: process.env.NEXT_PUBLIC_GQL || '',
URL_REST: process.env.NEXT_PUBLIC_REST || '',
URL_RPC: process.env.NEXT_PUBLIC_RPC || '',
URL_API: process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}/api`
: process.env.NEXT_PUBLIC_API,
WALLETS: process.env.NEXT_PUBLIC_WALLETS?.split(','),
: process.env.NEXT_PUBLIC_API || '',
URL_APOLLO_APR: process.env.NEXT_PUBLIC_APOLLO_APR || '',
WALLETS: process.env.NEXT_PUBLIC_WALLETS?.split(',') || [],
}
export const VERCEL_BYPASS = process.env.NEXT_PUBLIC_BYPASS
@ -39,14 +41,3 @@ export const VERCEL_BYPASS = process.env.NEXT_PUBLIC_BYPASS
: ''
export const IS_TESTNET = ENV.NETWORK !== 'mainnet'
export const ENV_MISSING_MESSAGE = () => {
const missing: string[] = []
Object.keys(ENV).forEach((key) => {
if (!ENV[key as keyof EnvironmentVariables]) {
missing.push(key)
}
})
return `Environment variable(s) missing for: ${missing.join(', ')}`
}

View File

@ -0,0 +1,10 @@
import useSWR from 'swr'
import getAccountDebts from 'api/accounts/getAccountDebts'
export default function useAccountDebts(accountId?: string) {
return useSWR(`accountDebts${accountId}`, () => getAccountDebts(accountId || ''), {
suspense: true,
isPaused: () => !accountId,
})
}

10
src/hooks/useAccounts.tsx Normal file
View File

@ -0,0 +1,10 @@
import useSWR from 'swr'
import getAccounts from 'api/wallets/getAccounts'
export default function useAccounts(address?: string) {
return useSWR(`accounts${address}`, () => getAccounts(address || ''), {
suspense: true,
isPaused: () => !address,
})
}

Some files were not shown because too many files have changed in this diff Show More