User account page (#22)

* layout, overview, start on assets, borrows and open orders

* trade history, sortable data hook for tables, borrow page

* handle deposit and withdraw buttons

* borrow modal ui and integration + settle borrow for individual assets

* in orders balance to asset table and totals, responsive css, new connected wallet button + small tweaks

* account switch/creation flow

* accounts modal, update to usebalances hook

* handle settle, deposit before settle, save last account

* disable borrow/withdraw button when no account
This commit is contained in:
saml33 2021-06-06 00:11:44 +10:00 committed by GitHub
parent bf0600ad6e
commit 3b5f22b815
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 4218 additions and 521 deletions

View File

@ -57,15 +57,13 @@ const AccountSelect = ({
)
const missingTokens = symbols
? Object.keys(symbols)
.filter((sym) => !symbolsForAccounts.includes(sym))
.join(', ')
? Object.keys(symbols).filter((sym) => !symbolsForAccounts.includes(sym))
: null
return (
<div className={`relative inline-block w-full`}>
<div className="flex justify-between pb-2">
<div className="text-th-fgd-1">Token Account</div>
<div className="text-th-fgd-1">Asset</div>
{accounts.length < Object.keys(symbols).length ? (
<button
className="ml-2 text-th-fgd-1 hover:text-th-primary outline-none focus:outline-none"
@ -121,7 +119,7 @@ const AccountSelect = ({
</div>
</div>
) : (
'Select a token address'
'Select an asset'
)}
{open ? (
<ChevronUpIcon className="h-5 w-5 ml-2 text-th-primary" />
@ -134,12 +132,6 @@ const AccountSelect = ({
<Listbox.Options
className={`z-20 p-1 absolute right-0 top-13 bg-th-bkg-1 divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md w-full max-h-60 overflow-auto`}
>
<div className="flex justify-between">
<div className={`text-th-fgd-4 p-2`}>Accounts</div>
{!hideAddress ? (
<div className={`text-th-fgd-4 p-2`}>Balance</div>
) : null}
</div>
{accounts.map((account) => {
const symbolForAccount = getSymbolForTokenMintAddress(
account?.account?.mint.toString()
@ -183,13 +175,27 @@ const AccountSelect = ({
</Listbox.Option>
)
})}
{missingTokens && accounts.length !== Object.keys(symbols).length ? (
<Listbox.Option value="">
<div className="flex items-center justify-center text-th-fgd-1 p-2">
Wallet token address not found for: {missingTokens}
</div>
</Listbox.Option>
) : null}
{missingTokens && accounts.length !== Object.keys(symbols).length
? missingTokens.map((token) => (
<Listbox.Option disabled key={token} value={token}>
<div
className={`opacity-50 p-2 hover:cursor-not-allowed`}
>
<div className={`flex items-center text-th-fgd-1`}>
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${token.toLowerCase()}.svg`}
className="mr-2"
/>
<div className={`flex-grow text-left`}>{token}</div>
<div className={`text-xs`}>No wallet address</div>
</div>
</div>
</Listbox.Option>
))
: null}
</Listbox.Options>
</>
)}

View File

@ -0,0 +1,193 @@
import React, { FunctionComponent, useEffect, useState } from 'react'
import { RadioGroup } from '@headlessui/react'
import { CheckCircleIcon } from '@heroicons/react/solid'
import {
ChevronLeftIcon,
CurrencyDollarIcon,
PlusCircleIcon,
} from '@heroicons/react/outline'
import useMangoStore from '../stores/useMangoStore'
import { MarginAccount } from '@blockworks-foundation/mango-client'
import { abbreviateAddress } from '../utils'
import useLocalStorageState from '../hooks/useLocalStorageState'
import Modal from './Modal'
import { ElementTitle } from './styles'
import Button, { LinkButton } from './Button'
import NewAccount from './NewAccount'
interface AccountsModalProps {
onClose: () => void
isOpen: boolean
}
const AccountsModal: FunctionComponent<AccountsModalProps> = ({
isOpen,
onClose,
}) => {
const [showNewAccountForm, setShowNewAccountForm] = useState(false)
const [newAccPublicKey, setNewAccPublicKey] = useState(null)
const marginAccounts = useMangoStore((s) => s.marginAccounts)
const selectedMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.current
)
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const prices = useMangoStore((s) => s.selectedMangoGroup.prices)
const setMangoStore = useMangoStore((s) => s.set)
const [, setLastAccountViewed] = useLocalStorageState('lastAccountViewed')
const handleMarginAccountChange = (marginAccount: MarginAccount) => {
setLastAccountViewed(marginAccount.publicKey.toString())
setMangoStore((state) => {
state.selectedMarginAccount.current = marginAccount
})
onClose()
}
useEffect(() => {
if (newAccPublicKey) {
setMangoStore((state) => {
state.selectedMarginAccount.current = marginAccounts.find((ma) =>
ma.publicKey.equals(newAccPublicKey)
)
})
}
}, [marginAccounts, newAccPublicKey])
const handleNewAccountCreation = (newAccPublicKey) => {
if (newAccPublicKey) {
setNewAccPublicKey(newAccPublicKey)
}
setShowNewAccountForm(false)
}
const handleShowNewAccountForm = () => {
setNewAccPublicKey(null)
setShowNewAccountForm(true)
}
const getAccountInfo = (acc) => {
const accountEquity = acc
.computeValue(selectedMangoGroup, prices)
.toFixed(2)
let leverage = accountEquity
? (1 / (acc.getCollateralRatio(selectedMangoGroup, prices) - 1)).toFixed(
2
)
: '∞'
return (
<div className="text-th-fgd-3 text-xs">
${accountEquity}
<span className="px-1.5 text-th-fgd-4">|</span>
<span
className={
parseFloat(leverage) > 4
? 'text-th-green'
: parseFloat(leverage) > 2
? 'text-th-orange'
: 'text-th-green'
}
>
{leverage}x
</span>
</div>
)
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
{marginAccounts.length > 0 ? (
!showNewAccountForm ? (
<>
<Modal.Header>
<ElementTitle noMarignBottom>Margin Accounts</ElementTitle>
</Modal.Header>
<div className="flex items-center justify-between pb-3 text-th-fgd-1">
<div className="font-semibold">
{marginAccounts.length > 1
? 'Select an account'
: 'Your Account'}
</div>
<Button
className="text-xs flex items-center justify-center pt-0 pb-0 h-8 pl-3 pr-3"
onClick={() => handleShowNewAccountForm()}
>
<div className="flex items-center">
<PlusCircleIcon className="h-5 w-5 mr-1.5" />
New
</div>
</Button>
</div>
<RadioGroup
value={selectedMarginAccount}
onChange={(acc) => handleMarginAccountChange(acc)}
>
<RadioGroup.Label className="sr-only">
Select a Margin Account
</RadioGroup.Label>
<div className="space-y-2">
{marginAccounts.map((account, i) => (
<RadioGroup.Option
key={account.publicKey.toString()}
value={account}
className={({ checked }) =>
`${
checked
? 'bg-th-bkg-3 ring-1 ring-th-green ring-inset'
: 'bg-th-bkg-1'
}
relative rounded-md w-full px-3 py-3 cursor-pointer default-transition flex hover:bg-th-bkg-3 focus:outline-none`
}
>
{({ checked }) => (
<>
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<div className="text-sm">
<RadioGroup.Label className="cursor-pointer flex items-center text-th-fgd-1">
<CurrencyDollarIcon className="h-5 w-5 mr-2.5" />
<div>
<div className="pb-0.5">
{abbreviateAddress(account.publicKey)}
</div>
{prices && selectedMangoGroup ? (
<div className="text-th-fgd-3 text-xs">
{getAccountInfo(account)}
</div>
) : null}
</div>
</RadioGroup.Label>
</div>
</div>
{checked && (
<div className="flex-shrink-0 text-th-green">
<CheckCircleIcon className="w-5 h-5" />
</div>
)}
</div>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</>
) : (
<>
<NewAccount onAccountCreation={handleNewAccountCreation} />
<LinkButton
className="flex items-center mt-4 text-th-fgd-3"
onClick={() => setShowNewAccountForm(false)}
>
<ChevronLeftIcon className="h-5 w-5 mr-1" />
Back
</LinkButton>
</>
)
) : (
<NewAccount onAccountCreation={handleNewAccountCreation} />
)}
</Modal>
)
}
export default React.memo(AccountsModal)

View File

@ -22,10 +22,8 @@ const AlertsList = () => {
const triggeredAlerts = useAlertsStore((s) => s.triggeredAlerts)
const loading = useAlertsStore((s) => s.loading)
const [
triggeredAlertsLength,
setTriggeredAlertsLength,
] = useLocalStorageState('triggeredAlertsLength', null)
const [triggeredAlertsLength, setTriggeredAlertsLength] =
useLocalStorageState('triggeredAlertsLength', null)
const [alertsCount, setAlertsCount] = useLocalStorageState('alertsCount', 0)
@ -105,10 +103,7 @@ const AlertsList = () => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel
static
className="absolute z-10 mt-4 right-0 md:transform md:-translate-x-1/2 md:left-1/2 w-64"
>
<Popover.Panel static className="absolute z-10 mt-4 right-0 w-64">
<div className="bg-th-bkg-1 p-4 overflow-auto max-h-80 rounded-lg shadow-lg thin-scroll">
{loading ? (
<div className="flex items-center justify-center text-th-primary h-40">

View File

@ -5,8 +5,6 @@ import useConnection from '../hooks/useConnection'
import Button from '../components/Button'
import { notify } from '../utils/notifications'
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
import useMarket from '../hooks/useMarket'
import { ElementTitle } from './styles'
import { InformationCircleIcon } from '@heroicons/react/outline'
import Tooltip from './Tooltip'
import { sleep } from '../utils'
@ -16,7 +14,6 @@ const BalancesTable = () => {
const balances = useBalances()
const { programId, connection } = useConnection()
const actions = useMangoStore((s) => s.actions)
const { marketName } = useMarket()
async function handleSettleAll() {
const markets = Object.values(
@ -56,15 +53,10 @@ const BalancesTable = () => {
}
return (
<div className={`flex flex-col py-6`}>
<div className={`flex flex-col py-4`}>
<div className={`-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8`}>
<div className={`align-middle inline-block min-w-full sm:px-6 lg:px-8`}>
<ElementTitle>
<div className="pr-1">{marketName.split('/')[0]}</div>
<span className="text-th-fgd-4">/</span>
<div className="pl-1">{marketName.split('/')[1]}</div>
</ElementTitle>
{balances.length &&
{balances.length > 0 &&
(balances.find(({ unsettled }) => unsettled > 0) ||
balances.find(
({ borrows, marginDeposits }) => borrows > 0 && marginDeposits > 0

634
components/BorrowModal.tsx Normal file
View File

@ -0,0 +1,634 @@
import React, { FunctionComponent, useEffect, useMemo, useState } from 'react'
import Modal from './Modal'
import Input from './Input'
import { ElementTitle } from './styles'
import useMangoStore from '../stores/useMangoStore'
import useMarketList from '../hooks/useMarketList'
import {
DECIMALS,
floorToDecimal,
tokenPrecision,
displayDepositsForMarginAccount,
} from '../utils/index'
import useConnection from '../hooks/useConnection'
import { borrowAndWithdraw, withdraw } from '../utils/mango'
import Loading from './Loading'
import Slider from './Slider'
import Button, { LinkButton } from './Button'
import { notify } from '../utils/notifications'
import Tooltip from './Tooltip'
import {
ExclamationCircleIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
import {
ChevronLeftIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@heroicons/react/solid'
import { Disclosure } from '@headlessui/react'
import { PublicKey } from '@solana/web3.js'
import { MarginAccount, uiToNative } from '@blockworks-foundation/mango-client'
import Select from './Select'
interface BorrowModalProps {
onClose: () => void
isOpen: boolean
tokenSymbol?: string
}
const BorrowModal: FunctionComponent<BorrowModalProps> = ({
isOpen,
onClose,
tokenSymbol = '',
}) => {
const [borrowTokenSymbol, setBorrowTokenSymbol] = useState(
tokenSymbol || 'USDC'
)
const [borrowAssetDetails, setBorrowAssetDetails] = useState(null)
const [inputAmount, setInputAmount] = useState(0)
const [invalidAmountMessage, setInvalidAmountMessage] = useState('')
const [maxAmount, setMaxAmount] = useState(0)
const [submitting, setSubmitting] = useState(false)
const [includeBorrow, setIncludeBorrow] = useState(false)
const [simulation, setSimulation] = useState(null)
const [showSimulation, setShowSimulation] = useState(false)
const [sliderPercentage, setSliderPercentage] = useState(0)
const [maxButtonTransition, setMaxButtonTransition] = useState(false)
const { getTokenIndex, symbols } = useMarketList()
const { connection, programId } = useConnection()
const prices = useMangoStore((s) => s.selectedMangoGroup.prices)
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const selectedMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.current
)
const actions = useMangoStore((s) => s.actions)
const tokenIndex = useMemo(
() => getTokenIndex(symbols[borrowTokenSymbol]),
[borrowTokenSymbol, getTokenIndex]
)
useEffect(() => {
if (!selectedMangoGroup || !selectedMarginAccount || !borrowTokenSymbol)
return
const mintDecimals = selectedMangoGroup.mintDecimals[tokenIndex]
const groupIndex = selectedMangoGroup.indexes[tokenIndex]
const deposits = selectedMarginAccount.getUiDeposit(
selectedMangoGroup,
tokenIndex
)
const borrows = selectedMarginAccount.getUiBorrow(
selectedMangoGroup,
tokenIndex
)
const currentAssetsVal =
selectedMarginAccount.getAssetsVal(selectedMangoGroup, prices) -
getMaxForSelectedAsset() * prices[tokenIndex]
const currentLiabs = selectedMarginAccount.getLiabsVal(
selectedMangoGroup,
prices
)
// multiply by 0.99 and subtract 0.01 to account for rounding issues
const liabsAvail = (currentAssetsVal / 1.2 - currentLiabs) * 0.99 - 0.01
// calculate max withdraw amount
const amountToWithdraw =
liabsAvail / prices[tokenIndex] + getMaxForSelectedAsset()
if (amountToWithdraw > 0) {
setMaxAmount(amountToWithdraw)
} else {
setMaxAmount(0)
}
// simulate change to deposits & borrow based on input amount
const newDeposit = Math.max(0, deposits - inputAmount)
const newBorrows = borrows + Math.max(0, inputAmount - deposits)
// clone MarginAccount and arrays to not modify selectedMarginAccount
const simulation = new MarginAccount(null, selectedMarginAccount)
simulation.deposits = [...selectedMarginAccount.deposits]
simulation.borrows = [...selectedMarginAccount.borrows]
// update with simulated values
simulation.deposits[tokenIndex] =
uiToNative(newDeposit, mintDecimals).toNumber() / groupIndex.deposit
simulation.borrows[tokenIndex] =
uiToNative(newBorrows, mintDecimals).toNumber() / groupIndex.borrow
const equity = simulation.computeValue(selectedMangoGroup, prices)
const assetsVal = simulation.getAssetsVal(selectedMangoGroup, prices)
const liabsVal = simulation.getLiabsVal(selectedMangoGroup, prices)
const collateralRatio = simulation.getCollateralRatio(
selectedMangoGroup,
prices
)
const leverage = 1 / Math.max(0, collateralRatio - 1)
setSimulation({
equity,
assetsVal,
liabsVal,
collateralRatio,
leverage,
})
}, [
includeBorrow,
inputAmount,
prices,
tokenIndex,
selectedMarginAccount,
selectedMangoGroup,
])
const handleWithdraw = () => {
setSubmitting(true)
const marginAccount = useMangoStore.getState().selectedMarginAccount.current
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const wallet = useMangoStore.getState().wallet.current
if (!marginAccount || !mangoGroup) return
if (!includeBorrow) {
withdraw(
connection,
new PublicKey(programId),
mangoGroup,
marginAccount,
wallet,
new PublicKey(symbols[borrowTokenSymbol]),
Number(inputAmount)
)
.then((_transSig: string) => {
setSubmitting(false)
actions.fetchMangoGroup()
actions.fetchMarginAccounts()
actions.fetchWalletBalances()
onClose()
})
.catch((err) => {
setSubmitting(false)
console.warn('Error withdrawing:', err)
notify({
message: 'Could not perform borrow and withdraw',
txid: err.txid,
type: 'error',
})
onClose()
})
} else {
borrowAndWithdraw(
connection,
new PublicKey(programId),
mangoGroup,
marginAccount,
wallet,
new PublicKey(symbols[borrowTokenSymbol]),
Number(inputAmount)
)
.then((_transSig: string) => {
setSubmitting(false)
actions.fetchMangoGroup()
actions.fetchMarginAccounts()
actions.fetchWalletBalances()
onClose()
})
.catch((err) => {
setSubmitting(false)
console.warn('Error borrowing and withdrawing:', err)
notify({
message: 'Could not perform borrow and withdraw',
description: `${err}`,
txid: err.txid,
type: 'error',
})
onClose()
})
}
}
const handleSetSelectedAsset = (symbol) => {
setInputAmount(0)
setSliderPercentage(0)
setBorrowTokenSymbol(symbol)
}
const getMaxForSelectedAsset = () => {
return displayDepositsForMarginAccount(
selectedMarginAccount,
selectedMangoGroup,
tokenIndex
)
}
const getBorrowAmount = () => {
const tokenBalance = getMaxForSelectedAsset()
const borrowAmount = inputAmount - tokenBalance
return borrowAmount > 0 ? borrowAmount : 0
}
const getAccountStatusColor = (
collateralRatio: number,
isRisk?: boolean,
isStatus?: boolean
) => {
if (collateralRatio < 1.25) {
return isRisk ? (
<div className="text-th-red">High</div>
) : isStatus ? (
'bg-th-red'
) : (
'border-th-red text-th-red'
)
} else if (collateralRatio > 1.25 && collateralRatio < 1.5) {
return isRisk ? (
<div className="text-th-orange">Moderate</div>
) : isStatus ? (
'bg-th-orange'
) : (
'border-th-orange text-th-orange'
)
} else {
return isRisk ? (
<div className="text-th-green">Low</div>
) : isStatus ? (
'bg-th-green'
) : (
'border-th-green text-th-green'
)
}
}
const setMaxBorrowForSelectedAsset = async () => {
setInputAmount(trimDecimals(maxAmount, DECIMALS[borrowTokenSymbol]))
setSliderPercentage(100)
setInvalidAmountMessage('')
setMaxButtonTransition(true)
}
const onChangeAmountInput = (amount) => {
setInputAmount(amount)
setSliderPercentage((amount / maxAmount) * 100)
setInvalidAmountMessage('')
}
const onChangeSlider = async (percentage) => {
const amount = (percentage / 100) * maxAmount
setInputAmount(trimDecimals(amount, DECIMALS[borrowTokenSymbol]))
setSliderPercentage(percentage)
setInvalidAmountMessage('')
}
const validateAmountInput = (e) => {
const amount = e.target.value
if (Number(amount) <= 0) {
setInvalidAmountMessage('Withdrawal amount must be greater than 0')
}
if (simulation.collateralRatio < 1.2) {
setInvalidAmountMessage(
'Leverage too high. Reduce the amount to withdraw'
)
}
}
const trimDecimals = (n, digits) => {
const step = Math.pow(10, digits || 0)
const temp = Math.trunc(step * n)
return temp / step
}
const getTokenBalances = () =>
Object.entries(symbols).map(([name], i) => {
return {
symbol: name,
balance: floorToDecimal(
selectedMarginAccount.getUiDeposit(selectedMangoGroup, i),
tokenPrecision[name]
),
}
})
// turn off slider transition for dragging slider handle interaction
useEffect(() => {
if (maxButtonTransition) {
setMaxButtonTransition(false)
}
}, [maxButtonTransition])
useEffect(() => {
const assetIndex = Object.keys(symbols).findIndex(
(a) => a === borrowTokenSymbol
)
const totalDeposits = selectedMangoGroup.getUiTotalDeposit(assetIndex)
const totalBorrows = selectedMangoGroup.getUiTotalBorrow(assetIndex)
setBorrowAssetDetails({
interest: selectedMangoGroup.getBorrowRate(assetIndex) * 100,
price: prices[assetIndex],
totalDeposits,
utilization: totalDeposits > 0.0 ? totalBorrows / totalDeposits : 0.0,
})
}, [borrowTokenSymbol])
if (!borrowTokenSymbol) return null
return (
<Modal isOpen={isOpen} onClose={onClose}>
<>
{!showSimulation ? (
<>
<Modal.Header>
<ElementTitle noMarignBottom>Borrow Funds</ElementTitle>
</Modal.Header>
<div className="pb-2 text-th-fgd-1">Asset</div>
<Select
value={
borrowTokenSymbol && selectedMarginAccount ? (
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${borrowTokenSymbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{borrowTokenSymbol}
</div>
{floorToDecimal(
selectedMarginAccount.getUiDeposit(
selectedMangoGroup,
tokenIndex
),
tokenPrecision[borrowTokenSymbol]
)}
</div>
) : (
<span className="text-th-fgd-4">Select an asset</span>
)
}
onChange={(asset) => handleSetSelectedAsset(asset)}
>
{getTokenBalances().map(({ symbol, balance }) => (
<Select.Option key={symbol} value={symbol}>
<div className="flex items-center justify-between">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${symbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<span>{symbol}</span>
</div>
{balance}
</div>
</Select.Option>
))}
</Select>
<div className="flex justify-between pb-2 pt-4">
<div className="text-th-fgd-1">Amount</div>
<div className="flex space-x-4">
<div
className="text-th-fgd-1 underline cursor-pointer default-transition hover:text-th-primary hover:no-underline"
onClick={setMaxBorrowForSelectedAsset}
>
Max
</div>
</div>
</div>
<div className="flex">
<Input
disabled={!borrowTokenSymbol}
type="number"
min="0"
className={`border border-th-fgd-4 flex-grow pr-11`}
error={!!invalidAmountMessage}
placeholder="0.00"
value={inputAmount}
onBlur={validateAmountInput}
onChange={(e) => onChangeAmountInput(e.target.value)}
suffix={borrowTokenSymbol}
/>
{simulation ? (
<Tooltip content="Projected Leverage" className="py-1">
<span
className={`${getAccountStatusColor(
simulation.collateralRatio
)} bg-th-bkg-1 border flex font-semibold h-10 items-center justify-center ml-2 rounded text-th-fgd-1 w-14`}
>
{simulation.leverage < 5
? simulation.leverage.toFixed(2)
: '>5'}
x
</span>
</Tooltip>
) : null}
</div>
{invalidAmountMessage ? (
<div className="flex items-center pt-1.5 text-th-red">
<ExclamationCircleIcon className="h-4 w-4 mr-1.5" />
{invalidAmountMessage}
</div>
) : null}
<div className="pt-3 pb-4">
<Slider
disabled={!borrowTokenSymbol}
value={sliderPercentage}
onChange={(v) => onChangeSlider(v)}
step={1}
maxButtonTransition={maxButtonTransition}
/>
</div>
<div className={`pt-8 flex justify-center`}>
<Button
onClick={() => setShowSimulation(true)}
disabled={
Number(inputAmount) <= 0 || simulation?.collateralRatio < 1.2
}
className="w-full"
>
Next
</Button>
</div>
</>
) : null}
{showSimulation && simulation ? (
<>
<Modal.Header>
<ElementTitle noMarignBottom>Confirm Borrow</ElementTitle>
</Modal.Header>
{simulation.collateralRatio < 1.2 ? (
<div className="border border-th-red mb-4 p-2 rounded">
<div className="flex items-center text-th-fgd-1">
<ExclamationCircleIcon className="h-4 w-4 mr-1.5 flex-shrink-0 text-th-red" />
Prices have changed and increased your leverage. Reduce the
borrow amount.
</div>
</div>
) : null}
<div className="bg-th-bkg-1 p-4 rounded-lg text-th-fgd-1 text-center">
<div className="text-th-fgd-3 pb-1">You're about to withdraw</div>
<div className="flex items-center justify-center">
<div className="font-semibold relative text-xl">
{inputAmount}
<span className="absolute bottom-0.5 font-normal ml-1.5 text-xs text-th-fgd-4">
{borrowTokenSymbol}
</span>
</div>
</div>
</div>
{getBorrowAmount() > 0 ? (
<div className="bg-th-bkg-1 mt-2 p-4 rounded-lg text-th-fgd-1 text-center">
<div className="flex justify-between pb-2">
<div className="text-th-fgd-4">Borrow Amount</div>
<div className="text-th-fgd-1">
{trimDecimals(
getBorrowAmount(),
DECIMALS[borrowTokenSymbol]
)}{' '}
{borrowTokenSymbol}
</div>
</div>
<div className="flex justify-between pb-2">
<div className="text-th-fgd-4">Interest APY</div>
<div className="text-th-fgd-1">
{borrowAssetDetails.interest.toFixed(2)}%
</div>
</div>
<div className="flex justify-between pb-2">
<div className="text-th-fgd-4">Price</div>
<div className="text-th-fgd-1">
${borrowAssetDetails.price}
</div>
</div>
<div className="flex justify-between">
<div className="text-th-fgd-4">Available Liquidity</div>
<div className="text-th-fgd-1">
{borrowAssetDetails.totalDeposits.toFixed(
DECIMALS[borrowTokenSymbol]
)}{' '}
{borrowTokenSymbol}
</div>
</div>
</div>
) : null}
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button
className={`border border-th-fgd-4 default-transition font-normal mt-4 pl-3 pr-2 py-2.5 ${
open ? 'rounded-b-none' : 'rounded-md'
} text-th-fgd-1 w-full hover:bg-th-bkg-3 focus:outline-none`}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<span className="flex h-2 w-2 mr-2.5 relative">
<span
className={`animate-ping absolute inline-flex h-full w-full rounded-full ${getAccountStatusColor(
simulation.collateralRatio,
false,
true
)} opacity-75`}
></span>
<span
className={`relative inline-flex rounded-full h-2 w-2 ${getAccountStatusColor(
simulation.collateralRatio,
false,
true
)}`}
></span>
</span>
Account Health Check
<Tooltip content="The details of your account after this withdrawal.">
<InformationCircleIcon
className={`h-5 w-5 ml-2 text-th-fgd-3 cursor-help`}
/>
</Tooltip>
</div>
{open ? (
<ChevronUpIcon className="h-5 w-5 mr-1" />
) : (
<ChevronDownIcon className="h-5 w-5 mr-1" />
)}
</div>
</Disclosure.Button>
<Disclosure.Panel
className={`border border-th-fgd-4 border-t-0 p-4 rounded-b-md`}
>
<div>
<div className="flex justify-between pb-2">
<div className="text-th-fgd-4">Account Value</div>
<div className="text-th-fgd-1">
${simulation.assetsVal.toFixed(2)}
</div>
</div>
<div className="flex justify-between pb-2">
<div className="text-th-fgd-4">Account Risk</div>
<div className="text-th-fgd-1">
{getAccountStatusColor(
simulation.collateralRatio,
true
)}
</div>
</div>
<div className="flex justify-between pb-2">
<div className="text-th-fgd-4">Leverage</div>
<div className="text-th-fgd-1">
{simulation.leverage.toFixed(2)}x
</div>
</div>
<div className="flex justify-between">
<div className="text-th-fgd-4">Collateral Ratio</div>
<div className="text-th-fgd-1">
{simulation.collateralRatio * 100 < 200
? Math.floor(simulation.collateralRatio * 100)
: '>200'}
%
</div>
</div>
{simulation.liabsVal > 0.05 ? (
<div className="flex justify-between pt-2">
<div className="text-th-fgd-4">Borrow Value</div>
<div className="text-th-fgd-1">
${simulation.liabsVal.toFixed(2)}
</div>
</div>
) : null}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
<div className={`mt-5 flex justify-center`}>
<Button
onClick={handleWithdraw}
disabled={
Number(inputAmount) <= 0 || simulation.collateralRatio < 1.2
}
className="w-full"
>
<div className={`flex items-center justify-center`}>
{submitting && <Loading className="-ml-1 mr-3" />}
Confirm
</div>
</Button>
</div>
<LinkButton
className="flex items-center mt-4 text-th-fgd-3"
onClick={() => setShowSimulation(false)}
>
<ChevronLeftIcon className="h-5 w-5 mr-1" />
Back
</LinkButton>
</>
) : null}
</>
</Modal>
)
}
export default React.memo(BorrowModal)

View File

@ -1,63 +1,33 @@
import { useEffect, useState } from 'react'
import { useCallback, useState } from 'react'
import styled from '@emotion/styled'
import useMangoStore from '../stores/useMangoStore'
import { Menu } from '@headlessui/react'
import { DuplicateIcon, LogoutIcon } from '@heroicons/react/outline'
import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/solid'
import {
CurrencyDollarIcon,
DuplicateIcon,
LogoutIcon,
} from '@heroicons/react/outline'
import { WALLET_PROVIDERS, DEFAULT_PROVIDER } from '../hooks/useWallet'
import useLocalStorageState from '../hooks/useLocalStorageState'
import { abbreviateAddress, copyToClipboard } from '../utils'
import WalletSelect from './WalletSelect'
import { WalletIcon } from './icons'
import { WalletIcon, ProfileIcon } from './icons'
import AccountsModal from './AccountsModal'
const StyledWalletTypeLabel = styled.div`
font-size: 0.65rem;
`
const StyledWalletButtonWrapper = styled.div`
width: 196px;
`
const Code = styled.code`
border: 1px solid hsla(0, 0%, 39.2%, 0.2);
border-radius: 3px;
background: hsla(0, 0%, 58.8%, 0.1);
font-size: 0.75rem;
`
const WALLET_OPTIONS = [
{ name: 'Copy address', icon: <DuplicateIcon /> },
{ name: 'Disconnect', icon: <LogoutIcon /> },
]
const ConnectWalletButton = () => {
const wallet = useMangoStore((s) => s.wallet.current)
const connected = useMangoStore((s) => s.wallet.connected)
const set = useMangoStore((s) => s.set)
const [isCopied, setIsCopied] = useState(false)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [savedProviderUrl] = useLocalStorageState(
'walletProvider',
DEFAULT_PROVIDER.url
)
useEffect(() => {
if (isCopied) {
const timer = setTimeout(() => {
setIsCopied(false)
}, 2000)
return () => clearTimeout(timer)
}
}, [isCopied])
const handleWalletMenu = (option) => {
if (option === 'Copy address') {
copyToClipboard(wallet.publicKey)
setIsCopied(true)
} else {
wallet.disconnect()
}
}
const handleWalletConect = () => {
wallet.connect()
set((state) => {
@ -65,49 +35,56 @@ const ConnectWalletButton = () => {
})
}
const handleCloseAccounts = useCallback(() => {
setShowAccountsModal(false)
}, [])
return (
<StyledWalletButtonWrapper className="h-14">
<>
{connected && wallet?.publicKey ? (
<Menu>
{({ open }) => (
<div className="relative h-full">
<Menu.Button className="h-full w-full px-3 bg-th-bkg-1 rounded-none focus:outline-none text-th-primary hover:bg-th-bkg-3 hover:text-th-fgd-1">
<div className="flex flex-row items-center justify-between">
<div className="flex items-center">
<WalletIcon className="w-4 h-4 mr-3 text-th-green fill-current" />
<Code className="p-1 text-th-fgd-3 font-light">
{isCopied
? 'Copied!'
: abbreviateAddress(wallet.publicKey)}
</Code>
<div className="relative h-full">
<Menu.Button className="bg-th-fgd-4 flex items-center justify-center rounded-full w-9 h-9 text-th-fgd-2 focus:outline-none hover:bg-th-bkg-3 hover:text-th-fgd-3">
<ProfileIcon className="fill-current h-5 w-5" />
</Menu.Button>
<Menu.Items className="bg-th-bkg-1 mt-2 p-1 absolute right-0 shadow-lg outline-none rounded-md w-48 z-20">
<Menu.Item>
<button
className="flex flex-row font-normal items-center rounded-none w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer focus:outline-none"
onClick={() => setShowAccountsModal(true)}
>
<CurrencyDollarIcon className="h-4 w-4" />
<div className="pl-2 text-left">Accounts</div>
</button>
</Menu.Item>
<Menu.Item>
<button
className="flex flex-row font-normal items-center rounded-none w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer focus:outline-none"
onClick={() => copyToClipboard(wallet?.publicKey)}
>
<DuplicateIcon className="h-4 w-4" />
<div className="pl-2 text-left">Copy address</div>
</button>
</Menu.Item>
<Menu.Item>
<button
className="flex flex-row font-normal items-center rounded-none w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer focus:outline-none"
onClick={() => wallet.disconnect()}
>
<LogoutIcon className="h-4 w-4" />
<div className="pl-2 text-left">
<div className="pb-0.5">Disconnect</div>
<div className="text-th-fgd-4 text-xs">
{abbreviateAddress(wallet?.publicKey)}
</div>
</div>
<div className="pl-2">
{open ? (
<ChevronUpIcon className="h-4 w-4" />
) : (
<ChevronDownIcon className="h-4 w-4" />
)}
</div>
</div>
</Menu.Button>
<Menu.Items className="z-20 mt-1 p-1 absolute right-0 md:transform md:-translate-x-1/2 md:left-1/2 bg-th-bkg-1 divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md w-48">
{WALLET_OPTIONS.map(({ name, icon }) => (
<Menu.Item key={name}>
<button
className="flex flex-row font-normal items-center rounded-none w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer focus:outline-none"
onClick={() => handleWalletMenu(name)}
>
<div className="w-4 h-4 mr-2">{icon}</div>
{name}
</button>
</Menu.Item>
))}
</Menu.Items>
</div>
)}
</button>
</Menu.Item>
</Menu.Items>
</div>
</Menu>
) : (
<div className="bg-th-bkg-1 h-full flex divide-x divide-th-bkg-3 justify-between">
<div className="bg-th-bkg-1 h-14 flex divide-x divide-th-bkg-3 justify-between">
<button
onClick={handleWalletConect}
disabled={!wallet}
@ -131,7 +108,13 @@ const ConnectWalletButton = () => {
</div>
</div>
)}
</StyledWalletButtonWrapper>
{showAccountsModal ? (
<AccountsModal
onClose={handleCloseAccounts}
isOpen={showAccountsModal}
/>
) : null}
</>
)
}

View File

@ -1,26 +1,60 @@
import React, { useMemo, useState } from 'react'
import React, { FunctionComponent, useEffect, useMemo, useState } from 'react'
import { Disclosure } from '@headlessui/react'
import {
ExclamationCircleIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
import {
ChevronLeftIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@heroicons/react/solid'
import {
nativeToUi,
sleep,
} from '@blockworks-foundation/mango-client/lib/utils'
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'
import { MarginAccount, uiToNative } from '@blockworks-foundation/mango-client'
import Modal from './Modal'
import Input from './Input'
import AccountSelect from './AccountSelect'
import { ElementTitle } from './styles'
import useMangoStore from '../stores/useMangoStore'
import useMarketList from '../hooks/useMarketList'
import { getSymbolForTokenMintAddress } from '../utils/index'
import {
getSymbolForTokenMintAddress,
DECIMALS,
trimDecimals,
} from '../utils/index'
import useConnection from '../hooks/useConnection'
import { deposit, initMarginAccountAndDeposit } from '../utils/mango'
import { PublicKey } from '@solana/web3.js'
import Loading from './Loading'
import Button from './Button'
import Button, { LinkButton } from './Button'
import Tooltip from './Tooltip'
import Slider from './Slider'
import InlineNotification from './InlineNotification'
import { notify } from '../utils/notifications'
const DepositModal = ({ isOpen, onClose }) => {
const [inputAmount, setInputAmount] = useState('')
interface DepositModalProps {
onClose: () => void
isOpen: boolean
settleDeficit?: number
tokenSymbol?: string
}
const DepositModal: FunctionComponent<DepositModalProps> = ({
isOpen,
onClose,
settleDeficit,
tokenSymbol = '',
}) => {
const [inputAmount, setInputAmount] = useState(settleDeficit || 0)
const [submitting, setSubmitting] = useState(false)
const [simulation, setSimulation] = useState(null)
const [showSimulation, setShowSimulation] = useState(false)
const [invalidAmountMessage, setInvalidAmountMessage] = useState('')
const [sliderPercentage, setSliderPercentage] = useState(0)
const [maxButtonTransition, setMaxButtonTransition] = useState(false)
const { getTokenIndex, symbols } = useMarketList()
const { connection, programId } = useConnection()
const mintDecimals = useMangoStore((s) => s.selectedMangoGroup.mintDecimals)
@ -35,24 +69,107 @@ const DepositModal = ({ isOpen, onClose }) => {
)
const [selectedAccount, setSelectedAccount] = useState(depositAccounts[0])
const prices = useMangoStore((s) => s.selectedMangoGroup.prices)
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const selectedMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.current
)
const mintAddress = useMemo(
() => selectedAccount?.account.mint.toString(),
[selectedAccount]
)
const tokenIndex = useMemo(
() => getTokenIndex(mintAddress),
[mintAddress, getTokenIndex]
)
const symbol = getSymbolForTokenMintAddress(
selectedAccount?.account?.mint.toString()
)
useEffect(() => {
if (tokenSymbol) {
const symbolMint = symbols[tokenSymbol]
const symbolAccount = walletAccounts.find(
(a) => a.account.mint.toString() === symbolMint
)
if (symbolAccount) {
setSelectedAccount(symbolAccount)
} else {
setSelectedAccount(null)
}
}
}, [tokenSymbol])
useEffect(() => {
if (!selectedMangoGroup || !selectedMarginAccount || !selectedAccount)
return
const mintDecimals = selectedMangoGroup.mintDecimals[tokenIndex]
const groupIndex = selectedMangoGroup.indexes[tokenIndex]
const deposits = selectedMarginAccount.getUiDeposit(
selectedMangoGroup,
tokenIndex
)
// simulate change to deposits based on input amount
const newDeposit = Math.max(0, +inputAmount + +deposits)
// clone MarginAccount and arrays to not modify selectedMarginAccount
const simulation = new MarginAccount(null, selectedMarginAccount)
simulation.deposits = [...selectedMarginAccount.deposits]
simulation.borrows = [...selectedMarginAccount.borrows]
// update with simulated values
simulation.deposits[tokenIndex] =
uiToNative(newDeposit, mintDecimals).toNumber() / groupIndex.deposit
const equity = simulation.computeValue(selectedMangoGroup, prices)
const assetsVal = simulation.getAssetsVal(selectedMangoGroup, prices)
const liabsVal = simulation.getLiabsVal(selectedMangoGroup, prices)
const collateralRatio = simulation.getCollateralRatio(
selectedMangoGroup,
prices
)
const leverage = 1 / Math.max(0, collateralRatio - 1)
setSimulation({
equity,
assetsVal,
liabsVal,
collateralRatio,
leverage,
})
}, [
inputAmount,
prices,
tokenIndex,
selectedMarginAccount,
selectedMangoGroup,
])
const handleAccountSelect = (account) => {
setInputAmount(0)
setSliderPercentage(0)
setInvalidAmountMessage('')
setSelectedAccount(account)
setInputAmount('')
}
// TODO: remove duplication in AccountSelect
const getBalanceForAccount = (account) => {
const mintAddress = account?.account.mint.toString()
const balance = nativeToUi(
(account?.account.mint.equals(WRAPPED_SOL_MINT)) ? Math.max(account?.account?.amount - (0.05 * 1e9), 0) : account?.account?.amount,
account?.account?.amount,
mintDecimals[getTokenIndex(mintAddress)]
)
return balance.toString()
return balance
}
const setMaxForSelectedAccount = () => {
const max = getBalanceForAccount(selectedAccount)
setInputAmount(max)
setSliderPercentage(100)
setInvalidAmountMessage('')
setMaxButtonTransition(true)
}
const handleDeposit = () => {
@ -85,7 +202,6 @@ const DepositModal = ({ isOpen, onClose }) => {
message:
'Could not perform init margin account and deposit operation',
type: 'error',
txid: err.txid
})
onClose()
})
@ -113,62 +229,291 @@ const DepositModal = ({ isOpen, onClose }) => {
notify({
message: 'Could not perform deposit operation',
type: 'error',
txid: err.txid
})
onClose()
})
}
}
const renderAccountRiskStatus = (
collateralRatio: number,
isRiskLevel?: boolean,
isStatusIcon?: boolean
) => {
if (collateralRatio < 1.25) {
return isRiskLevel ? (
<div className="text-th-red">High</div>
) : isStatusIcon ? (
'bg-th-red'
) : (
'border-th-red text-th-red'
)
} else if (collateralRatio > 1.25 && collateralRatio < 1.5) {
return isRiskLevel ? (
<div className="text-th-orange">Moderate</div>
) : isStatusIcon ? (
'bg-th-orange'
) : (
'border-th-orange text-th-orange'
)
} else {
return isRiskLevel ? (
<div className="text-th-green">Low</div>
) : isStatusIcon ? (
'bg-th-green'
) : (
'border-th-green text-th-green'
)
}
}
const validateAmountInput = (e) => {
const amount = e.target.value
if (Number(amount) <= 0) {
setInvalidAmountMessage('Enter an amount to deposit')
}
if (Number(amount) > getBalanceForAccount(selectedAccount)) {
setInvalidAmountMessage(
'Insufficient balance. Reduce the amount to deposit'
)
}
}
const onChangeAmountInput = (amount) => {
const max = getBalanceForAccount(selectedAccount)
setInputAmount(amount)
setSliderPercentage((amount / max) * 100)
setInvalidAmountMessage('')
}
const onChangeSlider = async (percentage) => {
const max = getBalanceForAccount(selectedAccount)
const amount = (percentage / 100) * max
setInputAmount(trimDecimals(amount, DECIMALS[symbol]))
setSliderPercentage(percentage)
setInvalidAmountMessage('')
}
// turn off slider transition for dragging slider handle interaction
useEffect(() => {
if (maxButtonTransition) {
setMaxButtonTransition(false)
}
}, [maxButtonTransition])
return (
<Modal isOpen={isOpen} onClose={onClose}>
<Modal.Header>
<div className={`text-th-fgd-3 flex-shrink invisible w-5`}>X</div>
<ElementTitle noMarignBottom>Deposit Funds</ElementTitle>
</Modal.Header>
<>
<AccountSelect
symbols={symbols}
accounts={depositAccounts}
selectedAccount={selectedAccount}
onSelectAccount={handleAccountSelect}
/>
<div className="flex justify-between pb-2 pt-4">
<div className={`text-th-fgd-1`}>Amount</div>
<div
className="text-th-fgd-1 underline cursor-pointer default-transition hover:text-th-primary hover:no-underline"
onClick={setMaxForSelectedAccount}
>
Max
</div>
</div>
<div className="flex items-center">
<Input
type="number"
min="0"
className={`border border-th-fgd-4 flex-grow pr-11`}
placeholder="0.00"
value={inputAmount}
onChange={(e) => setInputAmount(e.target.value)}
suffix={getSymbolForTokenMintAddress(
selectedAccount?.account?.mint.toString()
)}
{!showSimulation ? (
<>
<Modal.Header>
<ElementTitle noMarignBottom>Deposit Funds</ElementTitle>
</Modal.Header>
{tokenSymbol && !selectedAccount ? (
<InlineNotification
desc={`Add ${tokenSymbol} to your wallet and fund it with ${tokenSymbol} to deposit.`}
title={`No ${tokenSymbol} wallet address found`}
type="error"
/>
) : null}
{settleDeficit ? (
<InlineNotification
desc={`Deposit ${settleDeficit} ${tokenSymbol} before settling your borrow.`}
title="Not enough balance to settle"
type="error"
/>
) : null}
<AccountSelect
symbols={symbols}
accounts={depositAccounts}
selectedAccount={selectedAccount}
onSelectAccount={handleAccountSelect}
/>
</div>
<div className={`mt-5 flex justify-center`}>
<Button onClick={handleDeposit} className="w-full">
<div className={`flex items-center justify-center`}>
{submitting && <Loading className="-ml-1 mr-3" />}
{`Deposit ${
inputAmount ? inputAmount : ''
} ${getSymbolForTokenMintAddress(
selectedAccount?.account?.mint.toString()
)}
`}
<div className="flex justify-between pb-2 pt-4">
<div className={`text-th-fgd-1`}>Amount</div>
<div
className="text-th-fgd-1 underline cursor-pointer default-transition hover:text-th-primary hover:no-underline"
onClick={setMaxForSelectedAccount}
>
Max
</div>
</Button>
</div>
</>
</div>
<div className="flex">
<Input
type="number"
min="0"
className={`border border-th-fgd-4 flex-grow pr-11`}
placeholder="0.00"
error={!!invalidAmountMessage}
onBlur={validateAmountInput}
value={inputAmount}
onChange={(e) => onChangeAmountInput(e.target.value)}
suffix={symbol}
/>
{simulation ? (
<Tooltip content="Projected Leverage" className="py-1">
<span
className={`${renderAccountRiskStatus(
simulation?.collateralRatio
)} bg-th-bkg-1 border flex font-semibold h-10 items-center justify-center ml-2 rounded text-th-fgd-1 w-14`}
>
{simulation?.leverage < 5
? simulation?.leverage.toFixed(2)
: '>5'}
x
</span>
</Tooltip>
) : null}
</div>
{invalidAmountMessage ? (
<div className="flex items-center pt-1.5 text-th-red">
<ExclamationCircleIcon className="h-4 w-4 mr-1.5" />
{invalidAmountMessage}
</div>
) : null}
<div className="pt-3 pb-4">
<Slider
disabled={null}
value={sliderPercentage}
onChange={(v) => onChangeSlider(v)}
step={1}
maxButtonTransition={maxButtonTransition}
/>
</div>
<div className={`pt-8 flex justify-center`}>
<Button
onClick={() => setShowSimulation(true)}
className="w-full"
disabled={
inputAmount <= 0 ||
inputAmount > getBalanceForAccount(selectedAccount)
}
>
Next
</Button>
</div>
</>
) : (
<>
<Modal.Header>
<ElementTitle noMarignBottom>Confirm Deposit</ElementTitle>
</Modal.Header>
<div className="bg-th-bkg-1 p-4 rounded-lg text-th-fgd-1 text-center">
<div className="text-th-fgd-3 pb-1">{`You're about to deposit`}</div>
<div className="flex items-center justify-center">
<div className="font-semibold relative text-xl">
{inputAmount}
<span className="absolute bottom-0.5 font-normal ml-1.5 text-xs text-th-fgd-4">
{symbol}
</span>
</div>
</div>
</div>
{simulation ? (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button
className={`border border-th-fgd-4 default-transition font-normal mt-4 pl-3 pr-2 py-2.5 ${
open ? 'rounded-b-none' : 'rounded-md'
} text-th-fgd-1 w-full hover:bg-th-bkg-3 focus:outline-none`}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<span className="flex h-2 w-2 mr-2.5 relative">
<span
className={`animate-ping absolute inline-flex h-full w-full rounded-full ${renderAccountRiskStatus(
simulation?.collateralRatio,
false,
true
)} opacity-75`}
></span>
<span
className={`relative inline-flex rounded-full h-2 w-2 ${renderAccountRiskStatus(
simulation?.collateralRatio,
false,
true
)}`}
></span>
</span>
Account Health Check
<Tooltip content="The details of your account after this deposit.">
<InformationCircleIcon
className={`h-5 w-5 ml-2 text-th-fgd-3 cursor-help`}
/>
</Tooltip>
</div>
{open ? (
<ChevronUpIcon className="h-5 w-5 mr-1" />
) : (
<ChevronDownIcon className="h-5 w-5 mr-1" />
)}
</div>
</Disclosure.Button>
<Disclosure.Panel
className={`border border-th-fgd-4 border-t-0 p-4 rounded-b-md`}
>
<div>
<div className="flex justify-between pb-2">
<div className="text-th-fgd-4">Account Value</div>
<div className="text-th-fgd-1">
${simulation?.assetsVal.toFixed(2)}
</div>
</div>
<div className="flex justify-between pb-2">
<div className="text-th-fgd-4">Account Risk</div>
<div className="text-th-fgd-1">
{renderAccountRiskStatus(
simulation?.collateralRatio,
true
)}
</div>
</div>
<div className="flex justify-between pb-2">
<div className="text-th-fgd-4">Leverage</div>
<div className="text-th-fgd-1">
{simulation?.leverage.toFixed(2)}x
</div>
</div>
<div className="flex justify-between">
<div className="text-th-fgd-4">Collateral Ratio</div>
<div className="text-th-fgd-1">
{simulation?.collateralRatio * 100 < 200
? Math.floor(simulation?.collateralRatio * 100)
: '>200'}
%
</div>
</div>
{simulation?.liabsVal > 0.05 ? (
<div className="flex justify-between pt-2">
<div className="text-th-fgd-4">Borrow Value</div>
<div className="text-th-fgd-1">
${simulation?.liabsVal.toFixed(2)}
</div>
</div>
) : null}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
) : null}
<div className={`mt-5 flex justify-center`}>
<Button onClick={handleDeposit} className="w-full">
<div className={`flex items-center justify-center`}>
{submitting && <Loading className="-ml-1 mr-3" />}
Confirm
</div>
</Button>
</div>
<LinkButton
className="flex items-center mt-4 text-th-fgd-3"
onClick={() => setShowSimulation(false)}
>
<ChevronLeftIcon className="h-5 w-5 mr-1" />
Back
</LinkButton>
</>
)}
</Modal>
)
}

View File

@ -5,6 +5,7 @@ import Tooltip from './Tooltip'
type DropMenuProps = {
button: ReactNode
buttonClassName?: string
disabled?: boolean
onChange: (...args: any[]) => any
options: Array<any>
toolTipContent?: string
@ -14,6 +15,7 @@ type DropMenuProps = {
const DropMenu: FunctionComponent<DropMenuProps> = ({
button,
buttonClassName,
disabled,
value,
onChange,
options,
@ -24,7 +26,10 @@ const DropMenu: FunctionComponent<DropMenuProps> = ({
<Listbox value={value} onChange={onChange}>
{({ open }) => (
<>
<Listbox.Button className={`${buttonClassName} default-transition`}>
<Listbox.Button
className={`${buttonClassName} default-transition`}
disabled={disabled}
>
{toolTipContent && !open ? (
<Tooltip content={toolTipContent} className="text-xs py-1">
{button}
@ -45,7 +50,7 @@ const DropMenu: FunctionComponent<DropMenuProps> = ({
leaveTo="opacity-0 translate-y-1"
>
<Listbox.Options
className={`absolute z-10 mt-4 p-1 right-0 md:transform md:-translate-x-1/2 md:left-1/2 w-24 bg-th-bkg-1 divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md`}
className={`absolute z-10 mt-4 p-1 right-0 w-24 bg-th-bkg-1 divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md`}
>
{options.map((option) => (
<Listbox.Option key={option.name} value={option.name}>

33
components/EmptyState.tsx Normal file
View File

@ -0,0 +1,33 @@
import { FunctionComponent, ReactNode } from 'react'
import Button from './Button'
interface EmptyStateProps {
buttonText?: string
icon: ReactNode
onClickButton?: () => void
desc?: string
title: string
}
const EmptyState: FunctionComponent<EmptyStateProps> = ({
buttonText,
icon,
onClickButton,
desc,
title,
}) => {
return (
<div className="flex flex-col items-center text-th-fgd-1 px-4 pb-2 rounded-lg">
<div className="w-6 h-6 mb-1 text-th-primary">{icon}</div>
<div className="font-bold text-lg pb-1">{title}</div>
{desc ? <p className="mb-0 text-center">{desc}</p> : null}
{buttonText && onClickButton ? (
<Button className="mt-2" onClick={onClickButton}>
{buttonText}
</Button>
) : null}
</div>
)
}
export default EmptyState

View File

@ -0,0 +1,44 @@
import { FunctionComponent } from 'react'
import {
CheckCircleIcon,
ExclamationCircleIcon,
ExclamationIcon,
} from '@heroicons/react/outline'
interface InlineNotificationProps {
desc?: string
title?: string
type: string
}
const InlineNotification: FunctionComponent<InlineNotificationProps> = ({
desc,
title,
type,
}) => (
<div
className={`border ${
type === 'error'
? 'border-th-red'
: type === 'success'
? 'border-th-green'
: 'border-th-orange'
} flex items-center mb-4 p-2.5 rounded-md`}
>
{type === 'error' ? (
<ExclamationCircleIcon className="flex-shrink-0 h-5 w-5 mr-2 text-th-red" />
) : null}
{type === 'success' ? (
<CheckCircleIcon className="flex-shrink-0 h-5 w-5 mr-2 text-th-green" />
) : null}
{type === 'warning' ? (
<ExclamationIcon className="flex-shrink-0 h-5 w-5 mr-2 text-th-orange" />
) : null}
<div>
<div className="pb-1 text-th-fgd-1">{title}</div>
<div className="font-normal text-th-fgd-3 text-xs">{desc}</div>
</div>
</div>
)
export default InlineNotification

View File

@ -51,17 +51,11 @@ const MarginAccountSelect = ({
className={className}
>
{marginAccounts.length ? (
marginAccounts
.slice()
.sort(
(a, b) =>
(a.publicKey.toBase58() > b.publicKey.toBase58() && 1) || -1
)
.map((ma, index) => (
<Select.Option key={index} value={ma.publicKey.toString()}>
{abbreviateAddress(ma.publicKey)}
</Select.Option>
))
marginAccounts.map((ma, index) => (
<Select.Option key={index} value={ma.publicKey.toString()}>
{abbreviateAddress(ma.publicKey)}
</Select.Option>
))
) : (
<Select.Option value className="text-th-fgd-4">
No Margin Accounts Found

View File

@ -1,23 +1,24 @@
import { useCallback, useState } from 'react'
import {
ExternalLinkIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
import Link from 'next/link'
import { Menu } from '@headlessui/react'
import { DotsHorizontalIcon } from '@heroicons/react/outline'
import FloatingElement from './FloatingElement'
import { ElementTitle } from './styles'
import useMangoStore from '../stores/useMangoStore'
import useMarketList from '../hooks/useMarketList'
import { floorToDecimal, tokenPrecision } from '../utils/index'
import {
abbreviateAddress,
floorToDecimal,
tokenPrecision,
} from '../utils/index'
import DepositModal from './DepositModal'
import WithdrawModal from './WithdrawModal'
import BorrowModal from './BorrowModal'
import Button from './Button'
import Tooltip from './Tooltip'
import MarginAccountSelect from './MarginAccountSelect'
import { MarginAccount } from '@blockworks-foundation/mango-client'
import AccountsModal from './AccountsModal'
export default function MarginBalances() {
const setMangoStore = useMangoStore((s) => s.set)
const marginAccounts = useMangoStore((s) => s.marginAccounts)
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const selectedMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.current
@ -30,6 +31,8 @@ export default function MarginBalances() {
const [showDepositModal, setShowDepositModal] = useState(false)
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
const [showBorrowModal, setShowBorrowModal] = useState(false)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const handleCloseDeposit = useCallback(() => {
setShowDepositModal(false)
@ -39,39 +42,75 @@ export default function MarginBalances() {
setShowWithdrawModal(false)
}, [])
const handleMarginAccountChange = (marginAccount: MarginAccount) => {
setMangoStore((state) => {
state.selectedMarginAccount.current = marginAccount
})
}
const handleCloseBorrow = useCallback(() => {
setShowBorrowModal(false)
}, [])
const handleCloseAccounts = useCallback(() => {
setShowAccountsModal(false)
}, [])
return (
<>
<FloatingElement>
<ElementTitle>
Margin Account
<Tooltip
content={
<AddressTooltip
owner={selectedMarginAccount?.owner.toString()}
marginAccount={selectedMarginAccount?.publicKey.toString()}
/>
}
>
<div>
<InformationCircleIcon
className={`h-5 w-5 ml-2 text-th-primary cursor-help`}
/>
<div className="flex justify-between pb-5">
<div className="w-8 h-8" />
<div className="flex flex-col items-center">
<ElementTitle noMarignBottom>Margin Account</ElementTitle>
{selectedMarginAccount ? (
<Link href={'/account'}>
<a className="pt-1 text-th-fgd-3 text-xs underline hover:no-underline">
{abbreviateAddress(selectedMarginAccount?.publicKey)}
</a>
</Link>
) : null}
</div>
<Menu>
<div className="relative h-full">
<Menu.Button
className="flex items-center justify-center rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={!connected}
>
<DotsHorizontalIcon className="w-5 h-5" />
</Menu.Button>
<Menu.Items className="bg-th-bkg-1 mt-2 p-1 absolute right-0 shadow-lg outline-none rounded-md w-48 z-20">
<Menu.Item>
<button
className="flex flex-row font-normal items-center rounded-none w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer focus:outline-none"
onClick={() => setShowAccountsModal(true)}
>
<div className="pl-2 text-left">Change Account</div>
</button>
</Menu.Item>
<Menu.Item>
<button
className="flex flex-row font-normal items-center rounded-none w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={!selectedMarginAccount}
onClick={() => setShowBorrowModal(true)}
>
<div className="pl-2 text-left">Borrow</div>
</button>
</Menu.Item>
<Menu.Item>
<button
className="flex flex-row font-normal items-center rounded-none w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer focus:outline-none"
onClick={() => setShowDepositModal(true)}
>
<div className="pl-2 text-left">Deposit</div>
</button>
</Menu.Item>
<Menu.Item>
<button
className="flex flex-row font-normal items-center rounded-none w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={!selectedMarginAccount}
onClick={() => setShowWithdrawModal(true)}
>
<div className="pl-2 text-left">Withdraw</div>
</button>
</Menu.Item>
</Menu.Items>
</div>
</Tooltip>
</ElementTitle>
<div>
{marginAccounts.length > 1 ? (
<MarginAccountSelect
onChange={handleMarginAccountChange}
className="mb-2"
/>
) : null}
</Menu>
</div>
{selectedMangoGroup ? (
<table className={`min-w-full`}>
@ -96,7 +135,10 @@ export default function MarginBalances() {
scope="col"
className="flex-auto font-normal flex justify-end items-center"
>
<Tooltip content="Deposit APR and Borrow APY">
<Tooltip
className="text-xs py-1"
content="Deposit APR and Borrow APY"
>
<div>Deposits / Borrows</div>
</Tooltip>
</th>
@ -147,27 +189,6 @@ export default function MarginBalances() {
</tbody>
</table>
) : null}
<div className={`flex justify-center items-center mt-4`}>
<Button
onClick={() => setShowDepositModal(true)}
className="w-1/2"
disabled={!connected || loadingMarginAccount}
>
<span>Deposit</span>
</Button>
<Button
onClick={() => setShowWithdrawModal(true)}
className="ml-4 w-1/2"
disabled={
!connected || !selectedMarginAccount || loadingMarginAccount
}
>
<span>Withdraw</span>
</Button>
</div>
<div className={`text-center mt-5 text-th-fgd-3 text-xs`}>
Settle funds in the Balances tab
</div>
</FloatingElement>
{showDepositModal && (
<DepositModal isOpen={showDepositModal} onClose={handleCloseDeposit} />
@ -178,61 +199,15 @@ export default function MarginBalances() {
onClose={handleCloseWithdraw}
/>
)}
</>
)
}
const AddressTooltip = ({
owner,
marginAccount,
}: {
owner?: string
marginAccount?: string
}) => {
return (
<>
{owner && marginAccount ? (
<>
<div className={`flex flex-nowrap text-th-fgd-3`}>
Margin Account:
<a
className="text-th-fgd-1 default-transition hover:text-th-primary"
href={'https://explorer.solana.com/address/' + marginAccount}
target="_blank"
rel="noopener noreferrer"
>
<div className={`ml-2 flex items-center`}>
<span className={`underline`}>
{marginAccount.toString().substr(0, 5) +
'...' +
marginAccount.toString().substr(-5)}
</span>
<ExternalLinkIcon className={`h-4 w-4 ml-1`} />
</div>
</a>
</div>
<div className={`flex flex-nowrap text-th-fgd-3 pt-2`}>
Account Owner:
<a
className="text-th-fgd-1 default-transition hover:text-th-primary"
href={'https://explorer.solana.com/address/' + owner}
target="_blank"
rel="noopener noreferrer"
>
<div className={`ml-2 flex items-center`}>
<span className={`underline`}>
{owner.toString().substr(0, 5) +
'...' +
owner.toString().substr(-5)}
</span>
<ExternalLinkIcon className={`h-4 w-4 ml-1`} />
</div>
</a>
</div>
</>
) : (
'Connect a wallet and deposit funds to start trading'
{showBorrowModal && (
<BorrowModal isOpen={showBorrowModal} onClose={handleCloseBorrow} />
)}
{showAccountsModal ? (
<AccountsModal
onClose={handleCloseAccounts}
isOpen={showAccountsModal}
/>
) : null}
</>
)
}

209
components/NewAccount.tsx Normal file
View File

@ -0,0 +1,209 @@
import React, { FunctionComponent, useEffect, useMemo, useState } from 'react'
import { ExclamationCircleIcon } from '@heroicons/react/outline'
import {
nativeToUi,
sleep,
} from '@blockworks-foundation/mango-client/lib/utils'
import Input from './Input'
import AccountSelect from './AccountSelect'
import { ElementTitle } from './styles'
import useMangoStore from '../stores/useMangoStore'
import useMarketList from '../hooks/useMarketList'
import {
getSymbolForTokenMintAddress,
DECIMALS,
trimDecimals,
} from '../utils/index'
import useConnection from '../hooks/useConnection'
import { initMarginAccountAndDeposit } from '../utils/mango'
import { PublicKey } from '@solana/web3.js'
import Loading from './Loading'
import Button from './Button'
import Slider from './Slider'
import { notify } from '../utils/notifications'
interface NewAccountProps {
onAccountCreation?: (x?) => void
}
const NewAccount: FunctionComponent<NewAccountProps> = ({
onAccountCreation,
}) => {
const [inputAmount, setInputAmount] = useState(0)
const [submitting, setSubmitting] = useState(false)
const [invalidAmountMessage, setInvalidAmountMessage] = useState('')
const [sliderPercentage, setSliderPercentage] = useState(0)
const [maxButtonTransition, setMaxButtonTransition] = useState(false)
const { getTokenIndex, symbols } = useMarketList()
const { connection, programId } = useConnection()
const mintDecimals = useMangoStore((s) => s.selectedMangoGroup.mintDecimals)
const walletAccounts = useMangoStore((s) => s.wallet.balances)
const actions = useMangoStore((s) => s.actions)
const depositAccounts = useMemo(
() =>
walletAccounts.filter((acc) =>
Object.values(symbols).includes(acc.account.mint.toString())
),
[symbols, walletAccounts]
)
const [selectedAccount, setSelectedAccount] = useState(depositAccounts[0])
const symbol = getSymbolForTokenMintAddress(
selectedAccount?.account?.mint.toString()
)
const handleAccountSelect = (account) => {
setInputAmount(0)
setSliderPercentage(0)
setInvalidAmountMessage('')
setSelectedAccount(account)
}
// TODO: remove duplication in AccountSelect
const getBalanceForAccount = (account) => {
const mintAddress = account?.account.mint.toString()
const balance = nativeToUi(
account?.account?.amount,
mintDecimals[getTokenIndex(mintAddress)]
)
return balance
}
const setMaxForSelectedAccount = () => {
const max = getBalanceForAccount(selectedAccount)
setInputAmount(max)
setSliderPercentage(100)
setInvalidAmountMessage('')
setMaxButtonTransition(true)
}
const handleNewAccountDeposit = () => {
setSubmitting(true)
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const wallet = useMangoStore.getState().wallet.current
initMarginAccountAndDeposit(
connection,
new PublicKey(programId),
mangoGroup,
wallet,
selectedAccount.account.mint,
selectedAccount.publicKey,
Number(inputAmount)
)
.then(async (_response: Array<any>) => {
await sleep(1000)
actions.fetchWalletBalances()
actions.fetchMarginAccounts()
setSubmitting(false)
onAccountCreation(_response[0].publicKey)
})
.catch((err) => {
setSubmitting(false)
console.error(err)
notify({
message:
'Could not perform init margin account and deposit operation',
type: 'error',
})
onAccountCreation()
})
}
const validateAmountInput = (e) => {
const amount = e.target.value
if (Number(amount) <= 0) {
setInvalidAmountMessage('Enter an amount to deposit')
}
if (Number(amount) > getBalanceForAccount(selectedAccount)) {
setInvalidAmountMessage(
'Insufficient balance. Reduce the amount to deposit'
)
}
}
const onChangeAmountInput = (amount) => {
const max = getBalanceForAccount(selectedAccount)
setInputAmount(amount)
setSliderPercentage((amount / max) * 100)
setInvalidAmountMessage('')
}
const onChangeSlider = async (percentage) => {
const max = getBalanceForAccount(selectedAccount)
const amount = (percentage / 100) * max
setInputAmount(trimDecimals(amount, DECIMALS[symbol]))
setSliderPercentage(percentage)
setInvalidAmountMessage('')
}
// turn off slider transition for dragging slider handle interaction
useEffect(() => {
if (maxButtonTransition) {
setMaxButtonTransition(false)
}
}, [maxButtonTransition])
return (
<>
<ElementTitle noMarignBottom>Create Margin Account</ElementTitle>
<div className="text-th-fgd-3 text-center pb-4 pt-2">
Make a deposit to initialize a new margin account
</div>
<AccountSelect
symbols={symbols}
accounts={depositAccounts}
selectedAccount={selectedAccount}
onSelectAccount={handleAccountSelect}
/>
<div className="flex justify-between pb-2 pt-4">
<div className={`text-th-fgd-1`}>Amount</div>
<div
className="text-th-fgd-1 underline cursor-pointer default-transition hover:text-th-primary hover:no-underline"
onClick={setMaxForSelectedAccount}
>
Max
</div>
</div>
<div className="flex">
<Input
type="number"
min="0"
className={`border border-th-fgd-4 flex-grow pr-11`}
placeholder="0.00"
error={!!invalidAmountMessage}
onBlur={validateAmountInput}
value={inputAmount}
onChange={(e) => onChangeAmountInput(e.target.value)}
suffix={symbol}
/>
</div>
{invalidAmountMessage ? (
<div className="flex items-center pt-1.5 text-th-red">
<ExclamationCircleIcon className="h-4 w-4 mr-1.5" />
{invalidAmountMessage}
</div>
) : null}
<div className="pt-3 pb-4">
<Slider
disabled={null}
value={sliderPercentage}
onChange={(v) => onChangeSlider(v)}
step={1}
maxButtonTransition={maxButtonTransition}
/>
</div>
<div className={`pt-8 flex justify-center`}>
<Button onClick={handleNewAccountDeposit} className="w-full">
<div className={`flex items-center justify-center`}>
{submitting && <Loading className="-ml-1 mr-3" />}
Create Account
</div>
</Button>
</div>
</>
)
}
export default NewAccount

View File

@ -1,8 +1,10 @@
import { useState } from 'react'
import { TrashIcon } from '@heroicons/react/outline'
import Link from 'next/link'
import { ArrowSmDownIcon } from '@heroicons/react/solid'
import { useRouter } from 'next/router'
import { useOpenOrders } from '../hooks/useOpenOrders'
import { cancelOrderAndSettle } from '../utils/mango'
import Button from './Button'
import Button, { LinkButton } from './Button'
import Loading from './Loading'
import { PublicKey } from '@solana/web3.js'
import useConnection from '../hooks/useConnection'
@ -10,19 +12,22 @@ import useMangoStore from '../stores/useMangoStore'
import { notify } from '../utils/notifications'
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
import SideBadge from './SideBadge'
import { useSortableData } from '../hooks/useSortableData'
const OpenOrdersTable = () => {
const { asPath } = useRouter()
const openOrders = useOpenOrders()
const { items, requestSort, sortConfig } = useSortableData(openOrders)
const [cancelId, setCancelId] = useState(null)
const { connection, programId } = useConnection()
const actions = useMangoStore((s) => s.actions)
const handleCancelOrder = async (order) => {
const wallet = useMangoStore.getState().wallet.current
const selectedMangoGroup = useMangoStore.getState().selectedMangoGroup
.current
const selectedMarginAccount = useMangoStore.getState().selectedMarginAccount
.current
const selectedMangoGroup =
useMangoStore.getState().selectedMangoGroup.current
const selectedMarginAccount =
useMangoStore.getState().selectedMarginAccount.current
setCancelId(order?.orderId)
try {
if (!selectedMangoGroup || !selectedMarginAccount) return
@ -50,39 +55,93 @@ const OpenOrdersTable = () => {
}
return (
<div className={`flex flex-col py-6`}>
<div className={`flex flex-col py-4`}>
<div className={`-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8`}>
<div className={`align-middle inline-block min-w-full sm:px-6 lg:px-8`}>
{openOrders && openOrders.length > 0 ? (
<div
className={`shadow overflow-hidden border-b border-th-bkg-2 sm:rounded-md`}
>
<div className={`shadow overflow-hidden border-b border-th-bkg-2`}>
<Table className={`min-w-full divide-y divide-th-bkg-2`}>
<Thead>
<Tr className="text-th-fgd-3">
<Tr className="text-th-fgd-3 text-xs">
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Market
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('marketName')}
>
Market
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'marketName'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Side
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('side')}
>
Side
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'side'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Size
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('size')}
>
Size
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'size'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Price
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('price')}
>
Price
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'price'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th scope="col" className={`relative px-6 py-3`}>
<span className={`sr-only`}>Edit</span>
@ -90,7 +149,7 @@ const OpenOrdersTable = () => {
</Tr>
</Thead>
<Tbody>
{openOrders.map((order, index) => (
{items.map((order, index) => (
<Tr
key={`${order.orderId}${order.side}`}
className={`border-b border-th-bkg-3
@ -98,37 +157,58 @@ const OpenOrdersTable = () => {
`}
>
<Td
className={`px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1`}
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
{order.marketName}
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${order.marketName
.split('/')[0]
.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<div>{order.marketName}</div>
</div>
</Td>
<Td
className={`px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1`}
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<SideBadge side={order.side} />
</Td>
<Td
className={`px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1`}
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
{order.size}
</Td>
<Td
className={`px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1`}
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
{order.price}
</Td>
<Td className={`px-6 py-4 whitespace-nowrap text-left`}>
<Button
onClick={() => handleCancelOrder(order)}
className={`flex items-center md:ml-auto px-2 py-1 text-xs`}
>
{cancelId + '' === order?.orderId + '' ? (
<Loading className="-ml-1 mr-3" />
) : (
<TrashIcon className={`h-4 w-4 mr-1`} />
)}
<span>Cancel</span>
</Button>
<Td className={`px-6 py-3 whitespace-nowrap text-left`}>
<div className={`flex justify-end`}>
{/* Todo: support order modification */}
{/* <Button
onClick={() =>
console.log('trigger modify order modal')
}
className={`text-xs pt-0 pb-0 h-8 pl-3 pr-3`}
>
Modify
</Button> */}
<Button
onClick={() => handleCancelOrder(order)}
className={`ml-3 text-xs pt-0 pb-0 h-8 pl-3 pr-3`}
>
{cancelId + '' === order?.orderId + '' ? (
<Loading />
) : (
<span>Cancel</span>
)}
</Button>
</div>
</Td>
</Tr>
))}
@ -139,7 +219,17 @@ const OpenOrdersTable = () => {
<div
className={`w-full text-center py-6 bg-th-bkg-1 text-th-fgd-3 rounded-md`}
>
No open orders
No open orders.
{asPath === '/account' ? (
<Link href={'/'}>
<a
className={`inline-flex ml-2 py-0
`}
>
Make a trade
</a>
</Link>
) : null}
</div>
)}
</div>

View File

@ -1,6 +1,6 @@
const PageBodyContainer = ({ children }) => (
<div className="min-h-screen grid grid-cols-12 gap-4 pb-10">
<div className="col-span-12 px-6 md:col-start-2 md:col-span-10 lg:col-start-3 lg:col-span-8 2xl:col-start-5 2xl:col-span-6">
<div className="col-span-12 px-6 lg:col-start-2 lg:col-span-10 2xl:col-start-5 2xl:col-span-6">
{children}
</div>
</div>

View File

@ -7,7 +7,7 @@ type SideBadgeProps = {
const SideBadge: FunctionComponent<SideBadgeProps> = ({ side }) => {
return (
<div
className={`rounded-md inline-block ${
className={`rounded inline-block ${
side === 'buy'
? 'border border-th-green text-th-green'
: 'border border-th-red text-th-red'

View File

@ -17,9 +17,7 @@ const ThemeSwitch = () => {
// When mounted on client, now we can show the UI
useEffect(() => setMounted(true), [])
if (!mounted) return null
return (
return mounted ? (
<DropMenu
button={
<div className="flex items-center justify-center rounded-full bg-th-bkg-3 w-8 h-8">
@ -32,6 +30,8 @@ const ThemeSwitch = () => {
options={THEMES}
toolTipContent="Change Theme"
/>
) : (
<div className="bg-th-bkg-3 rounded-full w-8 h-8" />
)
}

View File

@ -1,5 +1,9 @@
import { useState } from 'react'
import { MenuIcon, XIcon } from '@heroicons/react/outline'
import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/solid'
import { Menu } from '@headlessui/react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import MenuItem from './MenuItem'
import ThemeSwitch from './ThemeSwitch'
import useMangoStore from '../stores/useMangoStore'
@ -10,6 +14,7 @@ const TopBar = () => {
const connected = useMangoStore((s) => s.wallet.connected)
const wallet = useMangoStore((s) => s.wallet.current)
const [showMenu, setShowMenu] = useState(false)
const { asPath } = useRouter()
return (
<>
@ -24,11 +29,55 @@ const TopBar = () => {
alt="next"
/>
</div>
<div className={`hidden md:flex md:space-x-6 md:ml-4 py-2`}>
<div
className={`hidden md:flex md:items-center md:space-x-6 md:ml-4 py-2`}
>
<MenuItem href="/">Trade</MenuItem>
<MenuItem href="/stats">Stats</MenuItem>
<MenuItem href="/account">Account</MenuItem>
<MenuItem href="/borrow">Borrow</MenuItem>
<MenuItem href="/alerts">Alerts</MenuItem>
<MenuItem href="https://docs.mango.markets/">Learn</MenuItem>
<Menu>
{({ open }) => (
<div className="relative">
<Menu.Button className="flex items-center hover:text-th-primary focus:outline-none">
More
<div className="pl-1">
{open ? (
<ChevronUpIcon className="h-5 w-5" />
) : (
<ChevronDownIcon className="h-5 w-5" />
)}
</div>
</Menu.Button>
<Menu.Items className="absolute z-10 mt-4 p-1 right-0 md:transform md:-translate-x-1/2 md:left-1/2 w-24 bg-th-bkg-1 divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md">
<Menu.Item>
<Link href="/stats">
<a
className={`block text-th-fgd-1 font-bold items-center p-2 hover:text-th-primary hover:opacity-100
${
asPath === '/stats'
? `text-th-primary`
: `border-transparent hover:border-th-primary`
}
`}
>
Stats
</a>
</Link>
</Menu.Item>
<Menu.Item>
<Link href="https://docs.mango.markets/">
<a
className={`block text-th-fgd-1 font-bold items-center p-2 hover:text-th-primary hover:opacity-100`}
>
Learn
</a>
</Link>
</Menu.Item>
</Menu.Items>
</div>
)}
</Menu>
</div>
</div>
<div className="flex items-center">

View File

@ -1,12 +1,17 @@
import { ArrowSmDownIcon } from '@heroicons/react/solid'
import useTradeHistory from '../hooks/useTradeHistory'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
import SideBadge from './SideBadge'
// import useMangoStore from '../stores/useMangoStore'
// import Loading from './Loading'
import { LinkButton } from './Button'
import { useSortableData } from '../hooks/useSortableData'
const TradeHistoryTable = () => {
const { asPath } = useRouter()
const tradeHistory = useTradeHistory()
// const connected = useMangoStore((s) => s.wallet.connected)
const { items, requestSort, sortConfig } = useSortableData(tradeHistory)
const renderTradeDateTime = (timestamp) => {
const date = new Date(timestamp)
return (
@ -18,62 +23,178 @@ const TradeHistoryTable = () => {
}
return (
<div className={`flex flex-col py-6`}>
<div className={`flex flex-col py-4`}>
<div className={`-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8`}>
<div className={`align-middle inline-block min-w-full sm:px-6 lg:px-8`}>
{tradeHistory && tradeHistory.length ? (
<div
className={`shadow overflow-hidden border-b border-th-bkg-2 sm:rounded-md`}
>
<div className={`shadow overflow-hidden border-b border-th-bkg-2`}>
<Table className={`min-w-full divide-y divide-th-bkg-2`}>
<Thead>
<Tr>
<Tr className="text-th-fgd-3 text-xs">
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Market
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('market')}
>
Market
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'market'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Side
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('side')}
>
Side
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'side'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Size
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('size')}
>
Size
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'size'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Price
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('price')}
>
Price
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'price'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Liquidity
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('value')}
>
Value
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'value'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Fees
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('liquidity')}
>
Liquidity
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'liquidity'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Approx Date/Time
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('feeCost')}
>
Fee
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'feeCost'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('loadTimestamp')}
>
Approx Date
<ArrowSmDownIcon
className={`default-transition flex-shrink-0 h-4 w-4 ml-1 ${
sortConfig?.key === 'loadTimestamp'
? sortConfig.direction === 'ascending'
? 'transform rotate-180'
: 'transform rotate-360'
: null
}`}
/>
</LinkButton>
</Th>
</Tr>
</Thead>
<Tbody>
{tradeHistory.map((trade, index) => (
{items.map((trade, index) => (
<Tr
key={`${trade.orderId}${trade.side}${trade.uuid}`}
className={`border-b border-th-bkg-3
@ -83,7 +204,18 @@ const TradeHistoryTable = () => {
<Td
className={`px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1`}
>
{trade.marketName}
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${trade.marketName
.split('/')[0]
.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<div>{trade.marketName}</div>
</div>
</Td>
<Td
className={`px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1`}
@ -100,6 +232,11 @@ const TradeHistoryTable = () => {
>
{trade.price}
</Td>
<Td
className={`px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1`}
>
${(trade.price * trade.size).toFixed(2)}
</Td>
<Td
className={`px-6 py-4 whitespace-nowrap text-sm text-th-fgd-1`}
>
@ -126,7 +263,17 @@ const TradeHistoryTable = () => {
<div
className={`w-full text-center py-6 bg-th-bkg-1 text-th-fgd-3 rounded-md`}
>
No trade history
No trade history.
{asPath === '/account' ? (
<Link href={'/'}>
<a
className={`inline-flex ml-2 py-0
`}
>
Make a trade
</a>
</Link>
) : null}
</div>
)}
</div>

View File

@ -24,13 +24,13 @@ export const defaultLayouts = {
{ i: 'orderbook', x: 6, y: 0, w: 3, h: 17 },
{ i: 'tradeForm', x: 9, y: 0, w: 3, h: 12 },
{ i: 'marketTrades', x: 6, y: 1, w: 3, h: 13 },
{ i: 'balanceInfo', x: 9, y: 1, w: 3, h: 15 },
{ i: 'balanceInfo', x: 9, y: 1, w: 3, h: 13 },
{ i: 'userInfo', x: 0, y: 2, w: 9, h: 17 },
{ i: 'marginInfo', x: 9, y: 2, w: 3, h: 13 },
],
lg: [
{ i: 'tvChart', x: 0, y: 0, w: 8, h: 28, minW: 2 },
{ i: 'balanceInfo', x: 8, y: 0, w: 4, h: 15, minW: 2 },
{ i: 'tvChart', x: 0, y: 0, w: 8, h: 26, minW: 2 },
{ i: 'balanceInfo', x: 8, y: 0, w: 4, h: 13, minW: 2 },
{ i: 'marginInfo', x: 8, y: 1, w: 4, h: 13, minW: 2 },
{ i: 'orderbook', x: 0, y: 2, w: 4, h: 17, minW: 2 },
{ i: 'tradeForm', x: 4, y: 2, w: 4, h: 17, minW: 3 },
@ -38,8 +38,8 @@ export const defaultLayouts = {
{ i: 'userInfo', x: 0, y: 3, w: 12, h: 17, minW: 6 },
],
md: [
{ i: 'tvChart', x: 0, y: 0, w: 8, h: 28, minW: 2 },
{ i: 'balanceInfo', x: 8, y: 0, w: 4, h: 15, minW: 2 },
{ i: 'tvChart', x: 0, y: 0, w: 8, h: 26, minW: 2 },
{ i: 'balanceInfo', x: 8, y: 0, w: 4, h: 13, minW: 2 },
{ i: 'marginInfo', x: 8, y: 1, w: 4, h: 13, minW: 2 },
{ i: 'orderbook', x: 0, y: 2, w: 4, h: 17, minW: 2 },
{ i: 'tradeForm', x: 4, y: 2, w: 4, h: 17, minW: 3 },
@ -48,8 +48,8 @@ export const defaultLayouts = {
],
sm: [
{ i: 'tvChart', x: 0, y: 0, w: 12, h: 25, minW: 6 },
{ i: 'balanceInfo', x: 0, y: 1, w: 6, h: 15, minW: 2 },
{ i: 'marginInfo', x: 6, y: 1, w: 6, h: 15, minW: 2 },
{ i: 'balanceInfo', x: 0, y: 1, w: 6, h: 13, minW: 2 },
{ i: 'marginInfo', x: 6, y: 1, w: 6, h: 13, minW: 2 },
{ i: 'tradeForm', x: 0, y: 2, w: 12, h: 13, minW: 3 },
{ i: 'orderbook', x: 0, y: 3, w: 6, h: 17, minW: 3 },
{ i: 'marketTrades', x: 6, y: 3, w: 6, h: 17, minW: 2 },
@ -57,7 +57,7 @@ export const defaultLayouts = {
],
xs: [
{ i: 'tvChart', x: 0, y: 0, w: 0, h: 0, minW: 6 },
{ i: 'balanceInfo', x: 0, y: 1, w: 6, h: 15, minW: 2 },
{ i: 'balanceInfo', x: 0, y: 1, w: 6, h: 13, minW: 2 },
{ i: 'marginInfo', x: 0, y: 2, w: 6, h: 13, minW: 2 },
{ i: 'tradeForm', x: 0, y: 3, w: 12, h: 13, minW: 3 },
{ i: 'orderbook', x: 0, y: 4, w: 6, h: 17, minW: 3 },

View File

@ -1,10 +1,11 @@
import React, { useEffect, useMemo, useState } from 'react'
import React, { FunctionComponent, useEffect, useMemo, useState } from 'react'
import Modal from './Modal'
import Input from './Input'
import { ElementTitle } from './styles'
import useMangoStore from '../stores/useMangoStore'
import useMarketList from '../hooks/useMarketList'
import {
DECIMALS,
floorToDecimal,
tokenPrecision,
displayDepositsForMarginAccount,
@ -31,8 +32,20 @@ import { PublicKey } from '@solana/web3.js'
import { MarginAccount, uiToNative } from '@blockworks-foundation/mango-client'
import Select from './Select'
const WithdrawModal = ({ isOpen, onClose }) => {
const [withdrawTokenSymbol, setWithdrawTokenSymbol] = useState('USDC')
interface WithdrawModalProps {
onClose: () => void
isOpen: boolean
tokenSymbol?: string
}
const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
isOpen,
onClose,
tokenSymbol = '',
}) => {
const [withdrawTokenSymbol, setWithdrawTokenSymbol] = useState(
tokenSymbol || 'USDC'
)
const [inputAmount, setInputAmount] = useState(0)
const [invalidAmountMessage, setInvalidAmountMessage] = useState('')
const [maxAmount, setMaxAmount] = useState(0)
@ -54,13 +67,6 @@ const WithdrawModal = ({ isOpen, onClose }) => {
() => getTokenIndex(symbols[withdrawTokenSymbol]),
[withdrawTokenSymbol, getTokenIndex]
)
const DECIMALS = {
BTC: 6,
ETH: 5,
SOL: 3,
SRM: 2,
USDC: 2,
}
useEffect(() => {
if (!selectedMangoGroup || !selectedMarginAccount || !withdrawTokenSymbol)
@ -346,7 +352,7 @@ const WithdrawModal = ({ isOpen, onClose }) => {
<div className="pb-2 text-th-fgd-1">Asset</div>
<Select
value={
withdrawTokenSymbol ? (
withdrawTokenSymbol && selectedMarginAccount ? (
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<img
@ -463,7 +469,7 @@ const WithdrawModal = ({ isOpen, onClose }) => {
maxButtonTransition={maxButtonTransition}
/>
</div>
<div className={`mt-5 flex justify-center`}>
<div className={`pt-8 flex justify-center`}>
<Button
onClick={() => setShowSimulation(true)}
disabled={

View File

@ -0,0 +1,281 @@
import { useCallback, useState } from 'react'
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
import { InformationCircleIcon } from '@heroicons/react/outline'
import useMangoStore from '../../stores/useMangoStore'
import { settleAllTrades } from '../../utils/mango'
import { useBalances } from '../../hooks/useBalances'
import useConnection from '../../hooks/useConnection'
import { tokenPrecision } from '../../utils/index'
import { notify } from '../../utils/notifications'
import { sleep } from '../../utils'
import { PublicKey } from '@solana/web3.js'
import DepositModal from '../DepositModal'
import WithdrawModal from '../WithdrawModal'
import Button from '../Button'
import Tooltip from '../Tooltip'
export default function AccountAssets() {
const balances = useBalances()
const { programId, connection } = useConnection()
const actions = useMangoStore((s) => s.actions)
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const selectedMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.current
)
const loadingMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.initialLoad
)
const connected = useMangoStore((s) => s.wallet.connected)
const prices = useMangoStore((s) => s.selectedMangoGroup.prices)
const [showDepositModal, setShowDepositModal] = useState(false)
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
const [withdrawSymbol, setWithdrawSymbol] = useState('')
const [depositSymbol, setDepositSymbol] = useState('')
const handleCloseDeposit = useCallback(() => {
setShowDepositModal(false)
}, [])
const handleCloseWithdraw = useCallback(() => {
setShowWithdrawModal(false)
}, [])
const handleShowWithdraw = (symbol) => {
setWithdrawSymbol(symbol)
setShowWithdrawModal(true)
}
const handleShowDeposit = (symbol) => {
setDepositSymbol(symbol)
setShowDepositModal(true)
}
async function handleSettleAllTrades() {
const markets = Object.values(
useMangoStore.getState().selectedMangoGroup.markets
)
const marginAccount = useMangoStore.getState().selectedMarginAccount.current
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const wallet = useMangoStore.getState().wallet.current
try {
await settleAllTrades(
connection,
new PublicKey(programId),
mangoGroup,
marginAccount,
markets,
wallet
)
await sleep(250)
actions.fetchMarginAccounts()
} catch (e) {
console.warn('Error settling all:', e)
if (e.message === 'No unsettled funds') {
notify({
message: 'There are no unsettled funds',
type: 'error',
})
} else {
notify({
message: 'Error settling funds',
description: e.message,
txid: e.txid,
type: 'error',
})
}
}
}
return selectedMarginAccount ? (
<>
<div className="sm:flex sm:items-center sm:justify-between pb-2">
<div className="pb-2 sm:pb-0 text-th-fgd-1 text-lg">Your Assets</div>
{balances.length > 0 ? (
<div className="border border-th-green flex items-center justify-between p-2 rounded">
<div className="pr-4 text-xs text-th-fgd-3">Total Asset Value:</div>
<span>
$
{balances
.reduce(
(acc, d, i) =>
acc +
(d.marginDeposits + d.orders + d.unsettled) * prices[i],
0
)
.toFixed(2)}
</span>
</div>
) : null}
</div>
{balances.length > 0 &&
balances.find(({ unsettled }) => unsettled > 0) ? (
<div
className={`flex items-center justify-between px-6 py-4 my-2 rounded-md bg-th-bkg-1`}
>
<div className="flex items-center text-fgd-1 font-semibold pr-4">
You have unsettled funds
<Tooltip content="Use the Settle All button to move unsettled funds to your deposits.">
<div>
<InformationCircleIcon
className={`h-5 w-5 ml-2 text-th-primary cursor-help`}
/>
</div>
</Tooltip>
</div>
<Button onClick={handleSettleAllTrades}>Settle All</Button>
</div>
) : null}
{selectedMangoGroup && balances.length > 0 ? (
<div className={`flex flex-col py-4`}>
<div className={`-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8`}>
<div
className={`align-middle inline-block min-w-full sm:px-6 lg:px-8`}
>
<Table className="min-w-full divide-y divide-th-bkg-2">
<Thead>
<Tr className="text-th-fgd-3 text-xs">
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Asset
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Available
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
In Orders
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Unsettled
</Th>
<Th
scope="col"
className={`px-6 py-3 text-left font-normal`}
>
Value
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Interest APR
</Th>
</Tr>
</Thead>
<Tbody>
{balances.map((bal, i) => (
<Tr
key={`${i}`}
className={`border-b border-th-bkg-3
${i % 2 === 0 ? `bg-th-bkg-3` : `bg-th-bkg-2`}
`}
>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${bal.coin.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<div>{bal.coin}</div>
</div>
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
{bal.marginDeposits.toFixed(tokenPrecision[bal.coin])}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
{bal.orders.toFixed(tokenPrecision[bal.coin])}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
{bal.unsettled.toFixed(tokenPrecision[bal.coin])}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
$
{(
(bal.marginDeposits + bal.orders + bal.unsettled) *
prices[i]
).toFixed(2)}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<span className={`text-th-green`}>
{(selectedMangoGroup.getDepositRate(i) * 100).toFixed(
2
)}
%
</span>
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<div className={`flex justify-end`}>
<Button
onClick={() => handleShowDeposit(bal.coin)}
className="text-xs pt-0 pb-0 h-8 pl-3 pr-3"
disabled={!connected || loadingMarginAccount}
>
<span>Deposit</span>
</Button>
<Button
onClick={() => handleShowWithdraw(bal.coin)}
className="ml-3 text-xs pt-0 pb-0 h-8 pl-3 pr-3"
disabled={!connected || loadingMarginAccount}
>
<span>Withdraw</span>
</Button>
</div>
</Td>
</Tr>
))}
</Tbody>
</Table>
</div>
</div>
</div>
) : (
<div
className={`w-full text-center py-6 bg-th-bkg-1 text-th-fgd-3 rounded-md`}
>
No assets found.
</div>
)}
{showDepositModal && (
<DepositModal
isOpen={showDepositModal}
onClose={handleCloseDeposit}
tokenSymbol={depositSymbol}
/>
)}
{showWithdrawModal && (
<WithdrawModal
isOpen={showWithdrawModal}
onClose={handleCloseWithdraw}
tokenSymbol={withdrawSymbol}
/>
)}
</>
) : null
}

View File

@ -0,0 +1,323 @@
import { useCallback, useState } from 'react'
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
import useConnection from '../../hooks/useConnection'
import useMangoStore from '../../stores/useMangoStore'
import useMarketList from '../../hooks/useMarketList'
import { useBalances } from '../../hooks/useBalances'
import { notify } from '../../utils/notifications'
import { sleep } from '../../utils'
import { PublicKey } from '@solana/web3.js'
import { floorToDecimal, tokenPrecision } from '../../utils/index'
import { settleBorrow } from '../../utils/mango'
import BorrowModal from '../BorrowModal'
import Button from '../Button'
import DepositModal from '../DepositModal'
export default function AccountBorrows() {
const balances = useBalances()
const { programId, connection } = useConnection()
const actions = useMangoStore((s) => s.actions)
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const selectedMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.current
)
const loadingMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.initialLoad
)
const connected = useMangoStore((s) => s.wallet.connected)
const { symbols } = useMarketList()
const prices = useMangoStore((s) => s.selectedMangoGroup.prices)
const [borrowSymbol, setBorrowSymbol] = useState('')
const [depositToSettle, setDepositToSettle] = useState(null)
const [showBorrowModal, setShowBorrowModal] = useState(false)
const [showDepositModal, setShowDepositModal] = useState(false)
async function handleSettleBorrow(token, borrowQuantity, depositBalance) {
const marginAccount = useMangoStore.getState().selectedMarginAccount.current
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const wallet = useMangoStore.getState().wallet.current
if (borrowQuantity > depositBalance) {
const deficit = borrowQuantity - depositBalance
handleShowDeposit(token, deficit)
return
}
try {
await settleBorrow(
connection,
new PublicKey(programId),
mangoGroup,
marginAccount,
wallet,
new PublicKey(symbols[token]),
Number(borrowQuantity)
)
await sleep(250)
actions.fetchMarginAccounts()
} catch (e) {
console.warn('Error settling all:', e)
if (e.message === 'No unsettled borrows') {
notify({
message: 'There are no unsettled borrows',
type: 'error',
})
} else {
notify({
message: 'Error settling borrows',
description: e.message,
txid: e.txid,
type: 'error',
})
}
}
}
const handleCloseWithdraw = useCallback(() => {
setShowBorrowModal(false)
}, [])
const handleCloseDeposit = useCallback(() => {
setDepositToSettle(null)
setShowDepositModal(false)
}, [])
const handleShowBorrow = (symbol) => {
setBorrowSymbol(symbol)
setShowBorrowModal(true)
}
const handleShowDeposit = (symbol, deficit) => {
setDepositToSettle({ symbol, deficit })
setShowDepositModal(true)
}
console.log(depositToSettle)
return (
<>
<div className="sm:flex sm:items-center sm:justify-between pb-4">
<div className="pb-2 sm:pb-0 text-th-fgd-1 text-lg">Your Borrows</div>
<div className="border border-th-red flex items-center justify-between p-2 rounded">
<div className="pr-4 text-xs text-th-fgd-3">Total Borrow Value:</div>
<span>
$
{balances
.reduce((acc, d, i) => acc + d.borrows * prices[i], 0)
.toFixed(2)}
</span>
</div>
</div>
{selectedMangoGroup ? (
balances.find((b) => b.borrows > 0) ? (
<Table className="min-w-full divide-y divide-th-bkg-2">
<Thead>
<Tr className="text-th-fgd-3 text-xs">
<Th scope="col" className={`px-6 py-3 text-left font-normal`}>
Asset
</Th>
<Th scope="col" className={`px-6 py-3 text-left font-normal`}>
Balance
</Th>
<Th scope="col" className={`px-6 py-3 text-left font-normal`}>
Value
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Interest APY
</Th>
</Tr>
</Thead>
<Tbody>
{balances
.filter((assets) => assets.borrows > 0)
.map((asset, i) => (
<Tr
key={`${i}`}
className={`border-b border-th-bkg-3
${i % 2 === 0 ? `bg-th-bkg-3` : `bg-th-bkg-2`}
`}
>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${asset.coin.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<div>{asset.coin}</div>
</div>
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
{asset.borrows.toFixed(tokenPrecision[asset.coin])}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
$
{(
asset.borrows *
prices[
Object.keys(symbols).findIndex(
(key) => key === asset.coin
)
]
).toFixed(tokenPrecision[asset.coin])}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<span className={`text-th-green`}>
{(
selectedMangoGroup.getBorrowRate(
Object.keys(symbols).findIndex(
(key) => key === asset.coin
)
) * 100
).toFixed(2)}
%
</span>
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<div className={`flex justify-end`}>
<Button
onClick={() =>
handleSettleBorrow(
asset.coin,
asset.borrows,
asset.marginDeposits
)
}
className="text-xs pt-0 pb-0 h-8 pl-3 pr-3"
disabled={
!connected ||
!selectedMarginAccount ||
loadingMarginAccount
}
>
Settle
</Button>
<Button
onClick={() => handleShowBorrow(asset.coin)}
className="ml-3 text-xs pt-0 pb-0 h-8 pl-3 pr-3"
disabled={!connected || loadingMarginAccount}
>
Borrow
</Button>
</div>
</Td>
</Tr>
))}
</Tbody>
</Table>
) : (
<div
className={`w-full text-center py-6 bg-th-bkg-1 text-th-fgd-3 rounded-md`}
>
No borrows found.
</div>
)
) : null}
<div className="pb-2 pt-8 text-th-fgd-1 text-lg">Available Assets</div>
<Table className="min-w-full divide-y divide-th-bkg-2">
<Thead>
<Tr className="text-th-fgd-3 text-xs">
<Th scope="col" className={`px-6 py-3 text-left font-normal`}>
Asset
</Th>
<Th scope="col" className={`px-6 py-3 text-left font-normal`}>
Price
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Interest APY
</Th>
<Th scope="col" className="px-6 py-3 text-left font-normal">
Available Liquidity
</Th>
</Tr>
</Thead>
<Tbody>
{Object.entries(symbols).map(([asset], i) => (
<Tr
key={`${i}`}
className={`border-b border-th-bkg-3
${i % 2 === 0 ? `bg-th-bkg-3` : `bg-th-bkg-2`}
`}
>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${asset.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<div>{asset}</div>
</div>
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
${prices[i]}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<span className={`text-th-green`}>
{(selectedMangoGroup.getBorrowRate(i) * 100).toFixed(2)}%
</span>
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
{selectedMangoGroup
.getUiTotalDeposit(i)
.toFixed(tokenPrecision[asset])}
</Td>
<Td
className={`px-6 py-3 whitespace-nowrap text-sm text-th-fgd-1`}
>
<div className={`flex justify-end`}>
<Button
onClick={() => handleShowBorrow(asset)}
className="text-xs pt-0 pb-0 h-8 pl-3 pr-3"
disabled={!connected || loadingMarginAccount}
>
Borrow
</Button>
</div>
</Td>
</Tr>
))}
</Tbody>
</Table>
{showBorrowModal && (
<BorrowModal
isOpen={showBorrowModal}
onClose={handleCloseWithdraw}
tokenSymbol={borrowSymbol}
/>
)}
{showDepositModal && (
<DepositModal
isOpen={showDepositModal}
onClose={handleCloseDeposit}
settleDeficit={depositToSettle.deficit}
tokenSymbol={depositToSettle.symbol}
/>
)}
</>
)
}

View File

@ -0,0 +1,49 @@
import { useState } from 'react'
import TradeHistoryTable from '../TradeHistoryTable'
const historyViews = ['Trades', 'Deposits', 'Withdrawals', 'Liquidations']
export default function AccountHistory() {
const [view, setView] = useState('Trades')
return (
<>
<div className="flex items-center justify-between pb-3.5 sm:pt-1">
<div className="text-th-fgd-1 text-lg">{view.slice(0, -1)} History</div>
{/* Todo: add this back when the data is available */}
{/* <div className="flex">
{historyViews.map((section) => (
<div
className={`border px-3 py-1.5 mr-2 rounded cursor-pointer default-transition
${
view === section
? `bg-th-bkg-3 border-th-bkg-3 text-th-primary`
: `border-th-fgd-4 text-th-fgd-1 opacity-80 hover:opacity-100`
}
`}
onClick={() => setView(section)}
key={section as string}
>
{section}
</div>
))}
</div> */}
</div>
<ViewContent view={view} />
</>
)
}
const ViewContent = ({ view }) => {
switch (view) {
case 'Trades':
return <TradeHistoryTable />
case 'Deposits':
return <div>Deposits</div>
case 'Withdrawals':
return <div>Withdrawals</div>
case 'Liquidations':
return <div>Liquidations</div>
default:
return <TradeHistoryTable />
}
}

View File

@ -0,0 +1,60 @@
import { useState } from 'react'
import { useOpenOrders } from '../../hooks/useOpenOrders'
import { cancelOrderAndSettle } from '../../utils/mango'
import Button from '../Button'
import Loading from '../Loading'
import { PublicKey } from '@solana/web3.js'
import useConnection from '../../hooks/useConnection'
import useMangoStore from '../../stores/useMangoStore'
import { notify } from '../../utils/notifications'
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'
import SideBadge from '../SideBadge'
import OpenOrdersTable from '../OpenOrdersTable'
const AccountOrders = () => {
const openOrders = useOpenOrders()
const [cancelId, setCancelId] = useState(null)
const { connection, programId } = useConnection()
const actions = useMangoStore((s) => s.actions)
const handleCancelOrder = async (order) => {
const wallet = useMangoStore.getState().wallet.current
const selectedMangoGroup =
useMangoStore.getState().selectedMangoGroup.current
const selectedMarginAccount =
useMangoStore.getState().selectedMarginAccount.current
setCancelId(order?.orderId)
try {
if (!selectedMangoGroup || !selectedMarginAccount) return
await cancelOrderAndSettle(
connection,
new PublicKey(programId),
selectedMangoGroup,
selectedMarginAccount,
wallet,
order.market,
order
)
actions.fetchMarginAccounts()
} catch (e) {
notify({
message: 'Error cancelling order',
description: e.message,
txid: e.txid,
type: 'error',
})
return
} finally {
setCancelId(null)
}
}
return (
<>
<div className="pb-3.5 sm:pt-1 text-th-fgd-1 text-lg">Open Orders</div>
<OpenOrdersTable />
</>
)
}
export default AccountOrders

View File

@ -120,3 +120,24 @@ export const TelegramIcon = ({ className }) => {
</svg>
)
}
export const ProfileIcon = ({ className }) => {
return (
<svg
className={`${className}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 45.532 45.532"
fill="currentColor"
>
<g>
<path
d="M22.766,0.001C10.194,0.001,0,10.193,0,22.766s10.193,22.765,22.766,22.765c12.574,0,22.766-10.192,22.766-22.765
S35.34,0.001,22.766,0.001z M22.766,6.808c4.16,0,7.531,3.372,7.531,7.53c0,4.159-3.371,7.53-7.531,7.53
c-4.158,0-7.529-3.371-7.529-7.53C15.237,10.18,18.608,6.808,22.766,6.808z M22.761,39.579c-4.149,0-7.949-1.511-10.88-4.012
c-0.714-0.609-1.126-1.502-1.126-2.439c0-4.217,3.413-7.592,7.631-7.592h8.762c4.219,0,7.619,3.375,7.619,7.592
c0,0.938-0.41,1.829-1.125,2.438C30.712,38.068,26.911,39.579,22.761,39.579z"
/>
</g>
</svg>
)
}

39
hooks/useAllMarkets.tsx Normal file
View File

@ -0,0 +1,39 @@
import useConnection from './useConnection'
import { IDS } from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore'
import { formatTokenMints } from './useMarket'
const useAllMarkets = () => {
const markets = useMangoStore((s) => s.selectedMangoGroup.markets)
const { cluster, programId } = useConnection()
const TOKEN_MINTS = formatTokenMints(IDS[cluster].symbols)
return Object.keys(markets).map(function (marketIndex) {
const market = markets[marketIndex]
const marketAddress = market ? market.publicKey.toString() : null
const baseCurrency =
(market?.baseMintAddress &&
TOKEN_MINTS.find((token) =>
token.address.equals(market.baseMintAddress)
)?.name) ||
'...'
const quoteCurrency =
(market?.quoteMintAddress &&
TOKEN_MINTS.find((token) =>
token.address.equals(market.quoteMintAddress)
)?.name) ||
'...'
return {
market,
marketAddress,
programId,
baseCurrency,
quoteCurrency,
}
})
}
export default useAllMarkets

View File

@ -1,4 +1,3 @@
import useMarket from './useMarket'
import { Balances } from '../@types/types'
import { nativeToUi } from '@blockworks-foundation/mango-client'
import useMarketList from './useMarketList'
@ -8,108 +7,123 @@ import {
displayDepositsForMarginAccount,
floorToDecimal,
} from '../utils'
import useAllMarkets from './useAllMarkets'
export function useBalances(): Balances[] {
const { baseCurrency, quoteCurrency, market } = useMarket()
let balances = []
const markets = useAllMarkets()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const marginAccount = useMangoStore((s) => s.selectedMarginAccount.current)
const { symbols } = useMarketList()
if (!marginAccount || !mangoGroup || !market) {
return []
}
for (const { market, baseCurrency, quoteCurrency } of markets) {
if (!marginAccount || !mangoGroup || !market) {
return []
}
const marketIndex = mangoGroup.getMarketIndex(market)
const openOrders: any = marginAccount.openOrdersAccounts[marketIndex]
const baseCurrencyIndex = Object.entries(symbols).findIndex(
(x) => x[0] === baseCurrency
)
const quoteCurrencyIndex = Object.entries(symbols).findIndex(
(x) => x[0] === quoteCurrency
)
if (
baseCurrency === 'UNKNOWN' ||
quoteCurrency === 'UNKNOWN' ||
!baseCurrency ||
!quoteCurrency
) {
return []
}
const nativeBaseFree = openOrders?.baseTokenFree || 0
const nativeQuoteFree = openOrders?.quoteTokenFree || 0
const nativeBaseLocked = openOrders
? openOrders.baseTokenTotal - nativeBaseFree
: 0
const nativeQuoteLocked = openOrders
? openOrders?.quoteTokenTotal - nativeQuoteFree
: 0
const nativeBaseUnsettled = openOrders?.baseTokenFree || 0
const nativeQuoteUnsettled = openOrders?.quoteTokenFree || 0
const tokenIndex = marketIndex
const net = (borrows, currencyIndex) => {
const amount =
marginAccount.getNativeDeposit(mangoGroup, currencyIndex) +
borrows -
marginAccount.getNativeBorrow(mangoGroup, currencyIndex)
return floorToDecimal(
nativeToUi(amount, mangoGroup.mintDecimals[currencyIndex]),
mangoGroup.mintDecimals[currencyIndex]
const marketIndex = mangoGroup.getMarketIndex(market)
const openOrders: any = marginAccount.openOrdersAccounts[marketIndex]
const baseCurrencyIndex = Object.entries(symbols).findIndex(
(x) => x[0] === baseCurrency
)
const quoteCurrencyIndex = Object.entries(symbols).findIndex(
(x) => x[0] === quoteCurrency
)
if (
baseCurrency === 'UNKNOWN' ||
quoteCurrency === 'UNKNOWN' ||
!baseCurrency ||
!quoteCurrency
) {
return []
}
const nativeBaseFree = openOrders?.baseTokenFree || 0
const nativeQuoteFree = openOrders?.quoteTokenFree || 0
const nativeBaseLocked = openOrders
? openOrders.baseTokenTotal - nativeBaseFree
: 0
const nativeQuoteLocked = openOrders
? openOrders?.quoteTokenTotal - nativeQuoteFree
: 0
const nativeBaseUnsettled = openOrders?.baseTokenFree || 0
const nativeQuoteUnsettled = openOrders?.quoteTokenFree || 0
const tokenIndex = marketIndex
const net = (borrows, currencyIndex) => {
const amount =
marginAccount.getNativeDeposit(mangoGroup, currencyIndex) +
borrows -
marginAccount.getNativeBorrow(mangoGroup, currencyIndex)
return floorToDecimal(
nativeToUi(amount, mangoGroup.mintDecimals[currencyIndex]),
mangoGroup.mintDecimals[currencyIndex]
)
}
const marketPair = [
{
market,
key: `${baseCurrency}${quoteCurrency}${baseCurrency}`,
coin: baseCurrency,
marginDeposits: displayDepositsForMarginAccount(
marginAccount,
mangoGroup,
baseCurrencyIndex
),
borrows: displayBorrowsForMarginAccount(
marginAccount,
mangoGroup,
baseCurrencyIndex
),
orders: nativeToUi(
nativeBaseLocked,
mangoGroup.mintDecimals[tokenIndex]
),
openOrders,
unsettled: nativeToUi(
nativeBaseUnsettled,
mangoGroup.mintDecimals[tokenIndex]
),
net: net(nativeBaseLocked, tokenIndex),
},
{
market,
key: `${quoteCurrency}${baseCurrency}${quoteCurrency}`,
coin: quoteCurrency,
marginDeposits: displayDepositsForMarginAccount(
marginAccount,
mangoGroup,
quoteCurrencyIndex
),
borrows: displayBorrowsForMarginAccount(
marginAccount,
mangoGroup,
quoteCurrencyIndex
),
openOrders,
orders: nativeToUi(
nativeQuoteLocked,
mangoGroup.mintDecimals[quoteCurrencyIndex]
),
unsettled: nativeToUi(
nativeQuoteUnsettled,
mangoGroup.mintDecimals[quoteCurrencyIndex]
),
net: net(nativeQuoteLocked, quoteCurrencyIndex),
},
]
balances = balances.concat(marketPair)
}
return [
{
market,
key: `${baseCurrency}${quoteCurrency}${baseCurrency}`,
coin: baseCurrency,
marginDeposits: displayDepositsForMarginAccount(
marginAccount,
mangoGroup,
baseCurrencyIndex
),
borrows: displayBorrowsForMarginAccount(
marginAccount,
mangoGroup,
baseCurrencyIndex
),
orders: nativeToUi(nativeBaseLocked, mangoGroup.mintDecimals[tokenIndex]),
openOrders,
unsettled: nativeToUi(
nativeBaseUnsettled,
mangoGroup.mintDecimals[tokenIndex]
),
net: net(nativeBaseLocked, tokenIndex),
},
{
market,
key: `${quoteCurrency}${baseCurrency}${quoteCurrency}`,
coin: quoteCurrency,
marginDeposits: displayDepositsForMarginAccount(
marginAccount,
mangoGroup,
quoteCurrencyIndex
),
borrows: displayBorrowsForMarginAccount(
marginAccount,
mangoGroup,
quoteCurrencyIndex
),
openOrders,
orders: nativeToUi(
nativeQuoteLocked,
mangoGroup.mintDecimals[quoteCurrencyIndex]
),
unsettled: nativeToUi(
nativeQuoteUnsettled,
mangoGroup.mintDecimals[quoteCurrencyIndex]
),
net: net(nativeQuoteLocked, quoteCurrencyIndex),
},
]
balances.sort((a, b) => (a.coin > b.coin ? 1 : -1))
balances = balances.filter(function (elem, index, self) {
return index === self.map((a) => a.coin).indexOf(elem.coin)
})
return balances
}

159
hooks/useMarginInfo.tsx Normal file
View File

@ -0,0 +1,159 @@
import { ReactNode, useEffect, useMemo, useState } from 'react'
import {
CurrencyDollarIcon,
ChartBarIcon,
ChartPieIcon,
ScaleIcon,
} from '@heroicons/react/outline'
import { nativeToUi } from '@blockworks-foundation/mango-client/lib/utils'
import { groupBy } from '../utils'
import useMangoStore from '../stores/useMangoStore'
import useTradeHistory from '../hooks/useTradeHistory'
const calculatePNL = (tradeHistory, prices, mangoGroup) => {
if (!tradeHistory.length) return '0.00'
const profitAndLoss = {}
const groupedTrades = groupBy(tradeHistory, (trade) => trade.marketName)
if (!prices.length) return '-'
const assetIndex = {
'BTC/USDT': 0,
'BTC/WUSDT': 0,
'ETH/USDT': 1,
'ETH/WUSDT': 1,
'SOL/USDT': 2,
'SOL/WUSDT': 2,
'SRM/USDT': 3,
'SRM/WUSDT': 3,
USDT: 2,
WUSDT: 2,
}
groupedTrades.forEach((val, key) => {
profitAndLoss[key] = val.reduce(
(acc, current) =>
(current.side === 'sell' ? current.size * -1 : current.size) + acc,
0
)
})
const totalNativeUsdt = tradeHistory.reduce((acc, current) => {
const usdtAmount =
current.side === 'sell'
? parseInt(current.nativeQuantityReleased)
: parseInt(current.nativeQuantityPaid) * -1
return usdtAmount + acc
}, 0)
profitAndLoss['USDT'] = nativeToUi(
totalNativeUsdt,
mangoGroup.mintDecimals[2]
)
let total = 0
for (const assetName in profitAndLoss) {
total = total + profitAndLoss[assetName] * prices[assetIndex[assetName]]
}
return isNaN(total) ? 0 : total.toFixed(2)
}
const useMarginInfo = () => {
const connection = useMangoStore((s) => s.connection.current)
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const selectedMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.current
)
const tradeHistory = useTradeHistory()
const tradeHistoryLength = useMemo(() => tradeHistory.length, [tradeHistory])
const [mAccountInfo, setMAccountInfo] =
useState<
| {
label: string
value: string
unit: string
desc: string
currency: string
icon: ReactNode
}[]
| null
>(null)
useEffect(() => {
if (selectedMangoGroup) {
selectedMangoGroup.getPrices(connection).then((prices) => {
const collateralRatio = selectedMarginAccount
? selectedMarginAccount.getCollateralRatio(selectedMangoGroup, prices)
: 200
const accountEquity = selectedMarginAccount
? selectedMarginAccount.computeValue(selectedMangoGroup, prices)
: 0
let leverage
if (selectedMarginAccount) {
leverage = accountEquity
? (
1 /
(selectedMarginAccount.getCollateralRatio(
selectedMangoGroup,
prices
) -
1)
).toFixed(2)
: '∞'
} else {
leverage = '0'
}
setMAccountInfo([
{
label: 'Account Value',
value: accountEquity.toFixed(2),
unit: '',
currency: '$',
desc: 'The value of the account',
icon: (
<CurrencyDollarIcon className="flex-shrink-0 h-5 w-5 mr-2 text-th-primary" />
),
},
{
label: 'Total PNL',
value: calculatePNL(tradeHistory, prices, selectedMangoGroup),
unit: '',
currency: '$',
desc: 'Total PNL reflects trades placed after March 15th 2021 04:00 AM UTC. Visit the Learn link in the top menu for more information.',
icon: (
<ChartBarIcon className="flex-shrink-0 h-5 w-5 mr-2 text-th-primary" />
),
},
{
label: 'Leverage',
value: leverage,
unit: 'x',
currency: '',
desc: 'Total position size divided by account value',
icon: (
<ScaleIcon className="flex-shrink-0 h-5 w-5 mr-2 text-th-primary" />
),
},
{
// TODO: Get collaterization ratio
label: 'Collateral Ratio',
value:
collateralRatio > 2 ? '>200' : (100 * collateralRatio).toFixed(0),
unit: '%',
currency: '',
desc: 'The current collateral ratio',
icon: (
<ChartPieIcon className="flex-shrink-0 h-5 w-5 mr-2 text-th-primary" />
),
},
])
})
}
}, [selectedMarginAccount, selectedMangoGroup, tradeHistoryLength])
return mAccountInfo
}
export default useMarginInfo

View File

@ -4,7 +4,7 @@ import { PublicKey } from '@solana/web3.js'
import useMangoStore from '../stores/useMangoStore'
import { IDS } from '@blockworks-foundation/mango-client'
const formatTokenMints = (symbols: { [name: string]: string }) => {
export const formatTokenMints = (symbols: { [name: string]: string }) => {
return Object.entries(symbols).map(([name, address]) => {
return {
address: new PublicKey(address),
@ -23,9 +23,10 @@ const useMarket = () => {
[market]
)
const TOKEN_MINTS = useMemo(() => formatTokenMints(IDS[cluster].symbols), [
cluster,
])
const TOKEN_MINTS = useMemo(
() => formatTokenMints(IDS[cluster].symbols),
[cluster]
)
const baseCurrency = useMemo(
() =>

40
hooks/useSortableData.tsx Normal file
View File

@ -0,0 +1,40 @@
import { useMemo, useState } from 'react'
export const useSortableData = (items, config = null) => {
const [sortConfig, setSortConfig] = useState(config)
const sortedItems = useMemo(() => {
let sortableItems = items ? [...items] : []
if (sortConfig !== null) {
sortableItems.sort((a, b) => {
if (!isNaN(a[sortConfig.key])) {
return sortConfig.direction === 'ascending'
? a[sortConfig.key] - b[sortConfig.key]
: b[sortConfig.key] - a[sortConfig.key]
}
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? -1 : 1
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? 1 : -1
}
return 0
})
}
return sortableItems
}, [items, sortConfig])
const requestSort = (key) => {
let direction = 'ascending'
if (
sortConfig &&
sortConfig.key === key &&
sortConfig.direction === 'ascending'
) {
direction = 'descending'
}
setSortConfig({ key, direction })
}
return { items: sortedItems, requestSort, sortConfig }
}

View File

@ -20,6 +20,7 @@ const formatTradeHistory = (newTradeHistory) => {
: `${trade.baseCurrency}/${trade.quoteCurrency}`,
key: `${trade.orderId}-${trade.uuid}`,
liquidity: trade.maker || trade?.eventFlags?.maker ? 'Maker' : 'Taker',
value: trade.price * trade.size,
}
})
.sort(byTimestamp)

167
pages/account.tsx Normal file
View File

@ -0,0 +1,167 @@
import { useCallback, useState } from 'react'
import {
CurrencyDollarIcon,
ExternalLinkIcon,
LinkIcon,
} from '@heroicons/react/outline'
import useMangoStore from '../stores/useMangoStore'
import { abbreviateAddress } from '../utils'
import useMarginInfo from '../hooks/useMarginInfo'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import AccountAssets from '../components/account-page/AccountAssets'
import AccountBorrows from '../components/account-page/AccountBorrows'
import AccountOrders from '../components/account-page/AccountOrders'
import AccountHistory from '../components/account-page/AccountHistory'
import AccountsModal from '../components/AccountsModal'
import EmptyState from '../components/EmptyState'
const TABS = [
'Assets',
'Borrows',
// 'Stats',
// 'Positions',
'Orders',
'History',
]
export default function Account() {
const [activeTab, setActiveTab] = useState(TABS[0])
const [showAccountsModal, setShowAccountsModal] = useState(false)
const accountMarginInfo = useMarginInfo()
const connected = useMangoStore((s) => s.wallet.connected)
const selectedMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.current
)
const handleTabChange = (tabName) => {
setActiveTab(tabName)
}
const handleCloseAccounts = useCallback(() => {
setShowAccountsModal(false)
}, [])
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />
<PageBodyContainer>
<div className="flex flex-col sm:flex-row items-center justify-between pt-8 pb-3 sm:pb-6 md:pt-10">
<h1 className={`text-th-fgd-1 text-2xl font-semibold`}>Account</h1>
{selectedMarginAccount ? (
<div className="divide-x divide-th-fgd-4 flex justify-center w-full pt-4 sm:pt-0 sm:justify-end">
<div className="pr-4 text-xs text-th-fgd-1">
<div className="pb-0.5 text-2xs text-th-fgd-3">Acc Owner</div>
<a
className="default-transition flex items-center text-th-fgd-2"
href={`https://explorer.solana.com/address/${selectedMarginAccount?.owner}`}
target="_blank"
rel="noopener noreferrer"
>
<span>{abbreviateAddress(selectedMarginAccount?.owner)}</span>
<ExternalLinkIcon className={`h-3 w-3 ml-1`} />
</a>
</div>
<div className="pl-4 text-xs text-th-fgd-1">
<div className="pb-0.5 text-2xs text-th-fgd-3">Acc Address</div>
<a
className="default-transition flex items-center text-th-fgd-2"
href={`https://explorer.solana.com/address/${selectedMarginAccount?.publicKey}`}
target="_blank"
rel="noopener noreferrer"
>
<span>
{abbreviateAddress(selectedMarginAccount?.publicKey)}
</span>
<ExternalLinkIcon className={`h-3 w-3 ml-1`} />
</a>
</div>
</div>
) : null}
</div>
<div className="bg-th-bkg-2 overflow-auto p-6 rounded-lg">
{selectedMarginAccount ? (
<>
<div className="pb-4 text-th-fgd-1 text-lg">Overview</div>
{accountMarginInfo ? (
<div className="grid grid-flow-col grid-cols-1 grid-rows-4 sm:grid-cols-2 sm:grid-rows-2 md:grid-cols-4 md:grid-rows-1 gap-4 pb-10">
{accountMarginInfo.map((info) => (
<div
className="bg-th-bkg-3 p-3 rounded-md"
key={info.label}
>
<div className="pb-0.5 text-xs text-th-fgd-3">
{info.label}
</div>
<div className="flex items-center">
{info.icon}
<div className="text-lg text-th-fgd-1">{`${info.currency}${info.value}${info.unit}`}</div>
</div>
</div>
))}
</div>
) : null}
<div className="border-b border-th-fgd-4 mb-4">
<nav className={`-mb-px flex space-x-6`} aria-label="Tabs">
{TABS.map((tabName) => (
<a
key={tabName}
onClick={() => handleTabChange(tabName)}
className={`whitespace-nowrap pb-4 px-1 border-b-2 font-semibold cursor-pointer default-transition hover:opacity-100
${
activeTab === tabName
? `border-th-primary text-th-primary`
: `border-transparent text-th-fgd-4 hover:text-th-primary`
}
`}
>
{tabName}
</a>
))}
</nav>
</div>
<TabContent activeTab={activeTab} />
</>
) : connected ? (
<EmptyState
buttonText="Create Account"
icon={<CurrencyDollarIcon />}
onClickButton={() => setShowAccountsModal(true)}
title="No Account Found"
/>
) : (
<EmptyState
desc="Connect a wallet to view your account"
icon={<LinkIcon />}
title="Connect Wallet"
/>
)}
</div>
</PageBodyContainer>
{showAccountsModal ? (
<AccountsModal
onClose={handleCloseAccounts}
isOpen={showAccountsModal}
/>
) : null}
</div>
)
}
const TabContent = ({ activeTab }) => {
switch (activeTab) {
case 'Assets':
return <AccountAssets />
case 'Borrows':
return <AccountBorrows />
case 'Stats':
return <div>Stats</div>
case 'Positions':
return <div>Positions</div>
case 'Orders':
return <AccountOrders />
case 'History':
return <AccountHistory />
default:
return <AccountAssets />
}
}

View File

@ -17,6 +17,7 @@ import Button, { LinkButton } from '../components/Button'
import AlertsModal from '../components/AlertsModal'
import AlertItem from '../components/AlertItem'
import PageBodyContainer from '../components/PageBodyContainer'
import EmptyState from '../components/EmptyState'
import { abbreviateAddress } from '../utils'
const relativeTime = require('dayjs/plugin/relativeTime')
@ -103,34 +104,27 @@ export default function Alerts() {
</button>
)}
</RadioGroup.Option>
{marginAccounts
.slice()
.sort(
(a, b) =>
(a.publicKey.toBase58() > b.publicKey.toBase58() && 1) ||
-1
)
.map((acc, i) => (
<RadioGroup.Option
value={acc.publicKey.toString()}
className="focus:outline-none flex-1"
key={i}
>
{({ checked }) => (
<button
className={`${
checked ? 'bg-th-bkg-3' : ''
} font-normal text-th-fgd-1 text-center py-1.5 h-full w-full rounded-none ${
i === marginAccounts.length - 1
? 'rounded-r-md'
: null
} border-l border-th-fgd-4 hover:bg-th-bkg-3 focus:outline-none`}
>
{abbreviateAddress(acc.publicKey)}
</button>
)}
</RadioGroup.Option>
))}
{marginAccounts.map((acc, i) => (
<RadioGroup.Option
value={acc.publicKey.toString()}
className="focus:outline-none flex-1"
key={i}
>
{({ checked }) => (
<button
className={`${
checked ? 'bg-th-bkg-3' : ''
} font-normal text-th-fgd-1 text-center py-1.5 h-full w-full rounded-none ${
i === marginAccounts.length - 1
? 'rounded-r-md'
: null
} border-l border-th-fgd-4 hover:bg-th-bkg-3 focus:outline-none`}
>
{abbreviateAddress(acc.publicKey)}
</button>
)}
</RadioGroup.Option>
))}
</RadioGroup>
) : null}
<Button
@ -189,13 +183,11 @@ export default function Alerts() {
/>
</>
) : (
<div className="flex flex-col items-center text-th-fgd-1 px-4 pb-2 rounded-lg">
<LinkIcon className="w-6 h-6 mb-1 text-th-primary" />
<div className="font-bold text-lg pb-1">Connect Wallet</div>
<p className="mb-0 text-center">
Connect your wallet to view and create liquidation alerts.
</p>
</div>
<EmptyState
desc="Connect a wallet to view and create liquidation alerts"
icon={<LinkIcon />}
title="Connect Wallet"
/>
)}
</div>
</PageBodyContainer>

55
pages/borrow.tsx Normal file
View File

@ -0,0 +1,55 @@
import { useCallback, useState } from 'react'
import { CurrencyDollarIcon, LinkIcon } from '@heroicons/react/outline'
import useMangoStore from '../stores/useMangoStore'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import EmptyState from '../components/EmptyState'
import AccountsModal from '../components/AccountsModal'
import AccountBorrows from '../components/account-page/AccountBorrows'
export default function Borrow() {
const [showAccountsModal, setShowAccountsModal] = useState(false)
const connected = useMangoStore((s) => s.wallet.connected)
const selectedMarginAccount = useMangoStore(
(s) => s.selectedMarginAccount.current
)
const handleCloseAccounts = useCallback(() => {
setShowAccountsModal(false)
}, [])
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />
<PageBodyContainer>
<div className="flex flex-col sm:flex-row pt-8 pb-3 sm:pb-6 md:pt-10">
<h1 className={`text-th-fgd-1 text-2xl font-semibold`}>
Borrow Funds
</h1>
</div>
<div className="p-6 rounded-lg bg-th-bkg-2">
{selectedMarginAccount ? (
<AccountBorrows />
) : connected ? (
<EmptyState
buttonText="Create Account"
icon={<CurrencyDollarIcon />}
onClickButton={() => setShowAccountsModal(true)}
title="No Account Found"
/>
) : (
<EmptyState
desc="Connect a wallet to view and create borrows"
icon={<LinkIcon />}
title="Connect Wallet"
/>
)}
</div>
</PageBodyContainer>
{showAccountsModal ? (
<AccountsModal
onClose={handleCloseAccounts}
isOpen={showAccountsModal}
/>
) : null}
</div>
)
}

View File

@ -0,0 +1,661 @@
{
"header": {
"reportVersion": 1,
"event": "Allocation failed - JavaScript heap out of memory",
"trigger": "FatalError",
"filename": "report.20210602.231048.64561.0.001.json",
"dumpEventTime": "2021-06-02T23:10:48Z",
"dumpEventTimeStamp": "1622639448044",
"processId": 64561,
"cwd": "/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2",
"commandLine": [
"node",
"/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2/node_modules/.bin/next",
"dev"
],
"nodejsVersion": "v12.14.0",
"wordSize": 64,
"arch": "x64",
"platform": "darwin",
"componentVersions": {
"node": "12.14.0",
"v8": "7.7.299.13-node.16",
"uv": "1.33.1",
"zlib": "1.2.11",
"brotli": "1.0.7",
"ares": "1.15.0",
"modules": "72",
"nghttp2": "1.39.2",
"napi": "5",
"llhttp": "1.1.4",
"http_parser": "2.8.0",
"openssl": "1.1.1d",
"cldr": "35.1",
"icu": "64.2",
"tz": "2019c",
"unicode": "12.1"
},
"release": {
"name": "node",
"lts": "Erbium",
"headersUrl": "https://nodejs.org/download/release/v12.14.0/node-v12.14.0-headers.tar.gz",
"sourceUrl": "https://nodejs.org/download/release/v12.14.0/node-v12.14.0.tar.gz"
},
"osName": "Darwin",
"osRelease": "16.4.0",
"osVersion": "Darwin Kernel Version 16.4.0: Thu Dec 22 22:53:21 PST 2016; root:xnu-3789.41.3~3/RELEASE_X86_64",
"osMachine": "x86_64",
"cpus": [
{
"model": "Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz",
"speed": 2700,
"user": 91006670,
"nice": 0,
"sys": 68670550,
"idle": 289634560,
"irq": 0
},
{
"model": "Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz",
"speed": 2700,
"user": 59652590,
"nice": 0,
"sys": 23759920,
"idle": 365882090,
"irq": 0
},
{
"model": "Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz",
"speed": 2700,
"user": 89189830,
"nice": 0,
"sys": 48334440,
"idle": 311770570,
"irq": 0
},
{
"model": "Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz",
"speed": 2700,
"user": 61353640,
"nice": 0,
"sys": 24618750,
"idle": 363322020,
"irq": 0
}
],
"networkInterfaces": [
{
"name": "lo0",
"internal": true,
"mac": "00:00:00:00:00:00",
"address": "127.0.0.1",
"netmask": "255.0.0.0",
"family": "IPv4"
},
{
"name": "lo0",
"internal": true,
"mac": "00:00:00:00:00:00",
"address": "::1",
"netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"family": "IPv6",
"scopeid": 0
},
{
"name": "lo0",
"internal": true,
"mac": "00:00:00:00:00:00",
"address": "fe80::1",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 1
},
{
"name": "en0",
"internal": false,
"mac": "d0:a6:37:ec:7f:ad",
"address": "fe80::181c:d777:5cef:a110",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 4
},
{
"name": "en0",
"internal": false,
"mac": "d0:a6:37:ec:7f:ad",
"address": "192.168.1.104",
"netmask": "255.255.255.0",
"family": "IPv4"
},
{
"name": "awdl0",
"internal": false,
"mac": "f6:d2:ae:16:b8:ba",
"address": "fe80::f4d2:aeff:fe16:b8ba",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 9
},
{
"name": "utun0",
"internal": false,
"mac": "00:00:00:00:00:00",
"address": "fe80::e2b9:f89c:9a5c:241b",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 10
}
],
"host": "Sams-MacBook-Pro.local"
},
"javascriptStack": {
"message": "No stack.",
"stack": [
"Unavailable."
]
},
"nativeStack": [
{
"pc": "0x000000010014db86",
"symbol": "report::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, v8::Local<v8::String>) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x000000010007eb13",
"symbol": "node::OnFatalError(char const*, char const*) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x0000000100176337",
"symbol": "v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001001762d3",
"symbol": "v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001002fa485",
"symbol": "v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001002fbb54",
"symbol": "v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001002f8a27",
"symbol": "v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001002f6a0d",
"symbol": "v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x0000000100302124",
"symbol": "v8::internal::Heap::AllocateRawWithLightRetry(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x000000010030219f",
"symbol": "v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001002ced97",
"symbol": "v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001005f83e5",
"symbol": "v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x0000000100930c99",
"symbol": "Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x000034df0a3ca47f",
"symbol": ""
},
{
"pc": "0x00000001008aa97c",
"symbol": "Builtins_ArgumentsAdaptorTrampoline [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001008b12e4",
"symbol": "Builtins_InterpreterEntryTrampoline [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001008b12e4",
"symbol": "Builtins_InterpreterEntryTrampoline [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001008aa97c",
"symbol": "Builtins_ArgumentsAdaptorTrampoline [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x0000000100943908",
"symbol": "Builtins_ArrayForEach [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
},
{
"pc": "0x00000001008b12e4",
"symbol": "Builtins_InterpreterEntryTrampoline [/Users/samluke/.nvm/versions/node/v12.14.0/bin/node]"
}
],
"javascriptHeap": {
"totalMemory": 2150522880,
"totalCommittedMemory": 2148776200,
"usedMemory": 2136440104,
"availableMemory": 50232728,
"memoryLimit": 2197815296,
"heapSpaces": {
"read_only_space": {
"memorySize": 262144,
"committedMemory": 32568,
"capacity": 261872,
"used": 32296,
"available": 229576
},
"new_space": {
"memorySize": 2097152,
"committedMemory": 1069536,
"capacity": 1047488,
"used": 19968,
"available": 1027520
},
"old_space": {
"memorySize": 394375168,
"committedMemory": 394201344,
"capacity": 387202864,
"used": 386567136,
"available": 635728
},
"code_space": {
"memorySize": 4620288,
"committedMemory": 4466240,
"capacity": 3824672,
"used": 3824672,
"available": 0
},
"map_space": {
"memorySize": 6819840,
"committedMemory": 6658224,
"capacity": 4320400,
"used": 4320400,
"available": 0
},
"large_object_space": {
"memorySize": 1741152256,
"committedMemory": 1741152256,
"capacity": 1740577872,
"used": 1740577872,
"available": 0
},
"code_large_object_space": {
"memorySize": 1196032,
"committedMemory": 1196032,
"capacity": 1097760,
"used": 1097760,
"available": 0
},
"new_large_object_space": {
"memorySize": 0,
"committedMemory": 0,
"capacity": 1047488,
"used": 0,
"available": 1047488
}
}
},
"resourceUsage": {
"userCpuSeconds": 805.165,
"kernelCpuSeconds": 198.771,
"cpuConsumptionPercent": 1.70023,
"maxRss": 736058408960,
"pageFaults": {
"IORequired": 2846,
"IONotRequired": 21268316
},
"fsActivity": {
"reads": 39477,
"writes": 6047
}
},
"libuv": [
],
"environmentVariables": {
"npm_package_devDependencies_lint_staged": "^10.0.10",
"npm_package_devDependencies_identity_obj_proxy": "^3.0.0",
"npm_package_devDependencies_prettier": "^2.0.2",
"TERM_PROGRAM": "Apple_Terminal",
"NODE": "/Users/samluke/.nvm/versions/node/v12.14.0/bin/node",
"npm_config_version_git_tag": "true",
"npm_package_devDependencies_typescript": "^4.1.3",
"NVM_CD_FLAGS": "",
"npm_package_dependencies_react_grid_layout": "^1.2.4",
"npm_package_devDependencies_jest": "^26.6.3",
"TERM": "xterm-256color",
"SHELL": "/bin/bash",
"npm_package_dependencies__emotion_styled": "^11.1.5",
"npm_package_dependencies__project_serum_serum": "^0.13.20",
"TMPDIR": "/var/folders/4s/95t514691qd938qmf1cxcj8m0000gn/T/",
"npm_config_init_license": "MIT",
"npm_package_scripts_lint": "eslint . --ext ts --ext tsx --ext js --quiet",
"CONDA_SHLVL": "1",
"Apple_PubSub_Socket_Render": "/private/tmp/com.apple.launchd.ealgMWYqCt/Render",
"CONDA_PROMPT_MODIFIER": "(base) ",
"TERM_PROGRAM_VERSION": "388",
"npm_package_scripts_dev": "next dev",
"npm_package_husky_hooks_pre_push": "",
"TERM_SESSION_ID": "DC2550B6-7F01-4BE8-B540-87BAA015E047",
"npm_config_registry": "https://registry.yarnpkg.com",
"npm_package_dependencies__headlessui_react": "^1.2.0",
"npm_package_dependencies__project_serum_sol_wallet_adapter": "^0.1.8",
"npm_package_dependencies__tippyjs_react": "^4.2.5",
"npm_package_dependencies_react_dom": "^17.0.1",
"npm_package_lint_staged_____ts_tsx__1": "yarn format",
"npm_package_dependencies_dayjs": "^1.10.4",
"npm_package_readmeFilename": "README.md",
"npm_config_python": "/usr/bin/python",
"npm_package_lint_staged_____ts_tsx__0": "yarn lint",
"npm_package_devDependencies__testing_library_react": "^11.2.5",
"npm_package_description": "Uses:",
"NVM_DIR": "/Users/samluke/.nvm",
"USER": "samluke",
"npm_package_license": "MIT",
"npm_package_devDependencies__types_react": "^17.0.1",
"npm_package_dependencies_bs58": "^4.0.1",
"npm_package_devDependencies__emotion_babel_plugin": "^11.2.0",
"npm_package_dependencies__solana_web3_js": "^0.90.5",
"CONDA_EXE": "/opt/anaconda3/bin/conda",
"npm_package_devDependencies__babel_core": "^7.13.10",
"npm_package_devDependencies_babel_jest": "^26.6.3",
"npm_package_dependencies_zustand": "^3.3.3",
"SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.ShYHBkXDSQ/Listeners",
"npm_package_devDependencies__types_jest": "^26.0.20",
"npm_package_devDependencies_babel_plugin_styled_components": "^1.12.0",
"npm_package_devDependencies_eslint": "^7.19.0",
"npm_package_devDependencies_postcss": "^8.2.8",
"__CF_USER_TEXT_ENCODING": "0x1F5:0x0:0xF",
"npm_package_husky_hooks_pre_commit": "lint-staged",
"npm_package_dependencies_buffer_layout": "^1.2.0",
"npm_package_dependencies_rc_slider": "^9.7.2",
"npm_package_devDependencies__typescript_eslint_eslint_plugin": "^4.14.2",
"npm_execpath": "/usr/local/Cellar/yarn/1.6.0/libexec/bin/yarn.js",
"npm_package_author_name": "@erikdstock",
"npm_package_dependencies_react_cool_dimensions": "^2.0.1",
"npm_package_scripts_type_check": "tsc --pretty --noEmit",
"npm_package_devDependencies__svgr_webpack": "^5.5.0",
"npm_package_devDependencies_twin_macro": "^2.4.1",
"_CE_CONDA": "",
"npm_package_dependencies__blockworks_foundation_mango_client": "https://github.com/blockworks-foundation/mango-client-ts#5_tokens",
"npm_package_devDependencies__typescript_eslint_parser": "^4.14.2",
"npm_package_dependencies_immer": "^9.0.1",
"npm_config_argv": "{\"remain\":[],\"cooked\":[\"run\",\"dev\"],\"original\":[\"dev\"]}",
"PATH": "/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2/node_modules/.bin:/Users/samluke/.config/yarn/link/node_modules/.bin:/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2/node_modules/.bin:/Users/samluke/.config/yarn/link/node_modules/.bin:/Users/samluke/.nvm/versions/node/v12.14.0/libexec/lib/node_modules/npm/bin/node-gyp-bin:/Users/samluke/.nvm/versions/node/v12.14.0/lib/node_modules/npm/bin/node-gyp-bin:/Users/samluke/.nvm/versions/node/v12.14.0/bin/node_modules/npm/bin/node-gyp-bin:/opt/anaconda3/bin:/opt/anaconda3/condabin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/Users/samluke/mongodb-macos-x86_64-4.2.3/bin:/Users/samluke/.nvm/versions/node/v12.14.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
"npm_package_dependencies_react_super_responsive_table": "^5.2.0",
"npm_package_devDependencies_babel_plugin_macros": "^3.1.0",
"_": "/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2/node_modules/.bin/next",
"npm_package_devDependencies__next_bundle_analyzer": "^10.1.3",
"CONDA_PREFIX": "/opt/anaconda3",
"npm_package_devDependencies_tailwindcss": "^2.1.2",
"PWD": "/Users/samluke/desktop/projects/mango/v3/mango-ui-v2",
"npm_package_devDependencies_eslint_plugin_react_hooks": "^4.2.0",
"npm_lifecycle_event": "dev",
"npm_package_name": "with-typescript-eslint-jest",
"npm_package_dependencies_immutable_tuple": "^0.4.10",
"LANG": "en_AU.UTF-8",
"npm_package_dependencies_react_phone_input_2": "^2.14.0",
"npm_package_scripts_build": "next build",
"npm_package_scripts_start": "next start",
"XPC_FLAGS": "0x0",
"npm_package_dependencies_next": "^10.1.3",
"npm_package_devDependencies_eslint_config_prettier": "^7.2.0",
"npm_package_version": "1.0.0",
"npm_package_dependencies__emotion_react": "^11.1.5",
"_CE_M": "",
"XPC_SERVICE_NAME": "0",
"npm_package_babelMacros_twin_preset": "styled-components",
"HOME": "/Users/samluke",
"SHLVL": "2",
"npm_package_scripts_test": "jest",
"npm_package_dependencies_postcss_preset_env": "^6.7.0",
"npm_config_strict_ssl": "true",
"npm_config_save_prefix": "^",
"npm_config_version_git_message": "v%s",
"npm_package_devDependencies_husky": "^4.2.3",
"npm_package_dependencies_bn_js": "^5.2.0",
"NPM_CONFIG_PYTHON": "/usr/bin/python",
"npm_package_dependencies__heroicons_react": "^1.0.0",
"npm_package_scripts_format": "prettier --check .",
"YARN_WRAP_OUTPUT": "false",
"CONDA_PYTHON_EXE": "/opt/anaconda3/bin/python",
"LOGNAME": "samluke",
"npm_package_devDependencies_react_is": "^17.0.2",
"npm_lifecycle_script": "next dev",
"PREFIX": "/usr/local",
"npm_package_dependencies_react": "^17.0.1",
"CONDA_DEFAULT_ENV": "base",
"NVM_BIN": "/Users/samluke/.nvm/versions/node/v12.14.0/bin",
"npm_config_user_agent": "yarn/1.6.0 npm/? node/v12.14.0 darwin x64",
"npm_config_ignore_scripts": "",
"npm_config_version_git_sign": "",
"npm_package_devDependencies_jest_watch_typeahead": "^0.6.1",
"npm_package_dependencies_babel_plugin_import": "^1.13.3",
"npm_package_dependencies_recharts": "^2.0.9",
"npm_package_devDependencies__types_node": "^14.14.25",
"npm_config_ignore_optional": "",
"npm_config_init_version": "1.0.0",
"SECURITYSESSIONID": "186a6",
"npm_package_scripts_test_all": "yarn lint && yarn type-check && yarn test",
"npm_config_version_tag_prefix": "v",
"npm_package_dependencies_next_themes": "^0.0.14",
"npm_package_dependencies_react_portal": "^4.2.1",
"npm_package_devDependencies_eslint_plugin_react": "^7.19.0",
"npm_node_execpath": "/Users/samluke/.nvm/versions/node/v12.14.0/bin/node",
"NODE_ENV": "development",
"TRACE_ID": "525e9cbb0be52e77"
},
"userLimits": {
"core_file_size_blocks": {
"soft": 0,
"hard": "unlimited"
},
"data_seg_size_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"file_size_blocks": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_locked_memory_bytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_memory_size_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"open_files": {
"soft": 10240,
"hard": "unlimited"
},
"stack_size_bytes": {
"soft": 8388608,
"hard": 67104768
},
"cpu_time_seconds": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_user_processes": {
"soft": 709,
"hard": 1064
},
"virtual_memory_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
}
},
"sharedObjects": [
"/Users/samluke/.nvm/versions/node/v12.14.0/bin/node",
"/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation",
"/usr/lib/libSystem.B.dylib",
"/usr/lib/libc++.1.dylib",
"/usr/lib/libDiagnosticMessagesClient.dylib",
"/usr/lib/libicucore.A.dylib",
"/usr/lib/libobjc.A.dylib",
"/usr/lib/libz.1.dylib",
"/usr/lib/system/libcache.dylib",
"/usr/lib/system/libcommonCrypto.dylib",
"/usr/lib/system/libcompiler_rt.dylib",
"/usr/lib/system/libcopyfile.dylib",
"/usr/lib/system/libcorecrypto.dylib",
"/usr/lib/system/libdispatch.dylib",
"/usr/lib/system/libdyld.dylib",
"/usr/lib/system/libkeymgr.dylib",
"/usr/lib/system/liblaunch.dylib",
"/usr/lib/system/libmacho.dylib",
"/usr/lib/system/libquarantine.dylib",
"/usr/lib/system/libremovefile.dylib",
"/usr/lib/system/libsystem_asl.dylib",
"/usr/lib/system/libsystem_blocks.dylib",
"/usr/lib/system/libsystem_c.dylib",
"/usr/lib/system/libsystem_configuration.dylib",
"/usr/lib/system/libsystem_coreservices.dylib",
"/usr/lib/system/libsystem_coretls.dylib",
"/usr/lib/system/libsystem_dnssd.dylib",
"/usr/lib/system/libsystem_info.dylib",
"/usr/lib/system/libsystem_kernel.dylib",
"/usr/lib/system/libsystem_m.dylib",
"/usr/lib/system/libsystem_malloc.dylib",
"/usr/lib/system/libsystem_network.dylib",
"/usr/lib/system/libsystem_networkextension.dylib",
"/usr/lib/system/libsystem_notify.dylib",
"/usr/lib/system/libsystem_platform.dylib",
"/usr/lib/system/libsystem_pthread.dylib",
"/usr/lib/system/libsystem_sandbox.dylib",
"/usr/lib/system/libsystem_secinit.dylib",
"/usr/lib/system/libsystem_symptoms.dylib",
"/usr/lib/system/libsystem_trace.dylib",
"/usr/lib/system/libunwind.dylib",
"/usr/lib/system/libxpc.dylib",
"/usr/lib/libauto.dylib",
"/usr/lib/libc++abi.dylib",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices",
"/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics",
"/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSync.framework/Versions/A/ColorSync",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/LangAnalysis.framework/Versions/A/LangAnalysis",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis",
"/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork",
"/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",
"/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation",
"/usr/lib/libbsm.0.dylib",
"/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration",
"/System/Library/Frameworks/Security.framework/Versions/A/Security",
"/usr/lib/libsqlite3.dylib",
"/usr/lib/libxml2.2.dylib",
"/usr/lib/libnetwork.dylib",
"/usr/lib/libenergytrace.dylib",
"/usr/lib/system/libkxld.dylib",
"/usr/lib/libpcap.A.dylib",
"/usr/lib/libcoretls.dylib",
"/usr/lib/libcoretls_cfhelpers.dylib",
"/usr/lib/libxar.1.dylib",
"/usr/lib/libpam.2.dylib",
"/usr/lib/libOpenScriptingUtil.dylib",
"/usr/lib/libbz2.1.0.dylib",
"/usr/lib/liblzma.5.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib",
"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices",
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList",
"/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration",
"/System/Library/Frameworks/NetFS.framework/Versions/A/NetFS",
"/System/Library/PrivateFrameworks/NetAuth.framework/Versions/A/NetAuth",
"/System/Library/PrivateFrameworks/login.framework/Versions/A/Frameworks/loginsupport.framework/Versions/A/loginsupport",
"/usr/lib/libarchive.2.dylib",
"/usr/lib/liblangid.dylib",
"/usr/lib/libCRFSuite.dylib",
"/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC",
"/usr/lib/libmecabra.dylib",
"/System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling",
"/usr/lib/libmarisa.dylib",
"/usr/lib/libChineseTokenizer.dylib",
"/usr/lib/libcmph.dylib",
"/usr/lib/libiconv.2.dylib",
"/System/Library/Frameworks/CoreData.framework/Versions/A/CoreData",
"/System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji",
"/usr/lib/libcompression.dylib",
"/System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory",
"/System/Library/Frameworks/ServiceManagement.framework/Versions/A/ServiceManagement",
"/usr/lib/libxslt.1.dylib",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontParser.dylib",
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib",
"/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib",
"/System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG",
"/System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface",
"/System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport",
"/usr/lib/libcups.2.dylib",
"/System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos",
"/System/Library/Frameworks/GSS.framework/Versions/A/GSS",
"/usr/lib/libresolv.9.dylib",
"/System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal",
"/usr/lib/libheimdal-asn1.dylib",
"/System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory",
"/System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth",
"/System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation",
"/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio",
"/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox",
"/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight",
"/System/Library/Frameworks/Metal.framework/Versions/A/Metal",
"/System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay",
"/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore",
"/System/Library/PrivateFrameworks/GPUCompiler.framework/libmetal_timestamp.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib",
"/System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator",
"/System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment",
"/System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage",
"/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL",
"/usr/lib/libFosl_dynamic.dylib",
"/System/Library/PrivateFrameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders",
"/System/Library/PrivateFrameworks/FaceCore.framework/Versions/A/FaceCore",
"/System/Library/Frameworks/OpenCL.framework/Versions/A/OpenCL",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib",
"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib",
"/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2/node_modules/fsevents/fsevents.node",
"/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2/node_modules/bufferutil/prebuilds/darwin-x64/node.napi.node",
"/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2/node_modules/utf-8-validate/prebuilds/darwin-x64/node.napi.node",
"/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2/node_modules/secp256k1/prebuilds/darwin-x64/node.napi.node",
"/Users/samluke/Desktop/Projects/mango/v3/mango-ui-v2/node_modules/keccak/prebuilds/darwin-x64/node.napi.node"
]
}

View File

@ -32,8 +32,7 @@ export const ENDPOINTS: EndpointInfo[] = [
type ClusterType = 'mainnet-beta' | 'devnet'
const CLUSTER =
(process.env.NEXT_PUBLIC_CLUSTER as ClusterType) || 'mainnet-beta'
const CLUSTER = (process.env.NEXT_PUBLIC_CLUSTER as ClusterType) || 'devnet'
const ENDPOINT = ENDPOINTS.find((e) => e.name === CLUSTER)
const DEFAULT_CONNECTION = new Connection(ENDPOINT.url, 'recent')
const WEBSOCKET_CONNECTION = new Connection(ENDPOINT.websocket, 'recent')
@ -250,8 +249,14 @@ const useMangoStore = create<MangoStore>((set, get) => ({
)
.then((marginAccounts) => {
if (marginAccounts.length > 0) {
const sortedAccounts = marginAccounts
.slice()
.sort(
(a, b) =>
(a.publicKey.toBase58() > b.publicKey.toBase58() && 1) || -1
)
set((state) => {
state.marginAccounts = marginAccounts
state.marginAccounts = sortedAccounts
if (state.selectedMarginAccount.current) {
state.selectedMarginAccount.current = marginAccounts.find(
(ma) =>
@ -260,7 +265,14 @@ const useMangoStore = create<MangoStore>((set, get) => ({
)
)
} else {
state.selectedMarginAccount.current = marginAccounts[0]
const lastAccount = localStorage.getItem('lastAccountViewed')
console.log(JSON.parse(lastAccount))
state.selectedMarginAccount.current = lastAccount
? marginAccounts.find(
(ma) =>
ma.publicKey.toString() === JSON.parse(lastAccount)
)
: sortedAccounts[0]
}
})
}

View File

@ -5,12 +5,13 @@
/* Theme */
:root {
--primary: theme('colors.light-theme.orange');
--primary: theme('colors.light-theme.orange.DEFAULT');
--primary-dark: theme('colors.light-theme.orange.dark');
--red: theme('colors.light-theme.red.DEFAULT');
--red-dark: theme('colors.light-theme.red.dark');
--green: theme('colors.light-theme.green.DEFAULT');
--green-dark: theme('colors.light-theme.green.dark');
--orange: theme('colors.light-theme.orange');
--orange: theme('colors.light-theme.orange.DEFAULT');
--bkg-1: theme('colors.light-theme["bkg-1"]');
--bkg-2: theme('colors.light-theme["bkg-2"]');
--bkg-3: theme('colors.light-theme["bkg-3"]');
@ -21,7 +22,8 @@
}
[data-theme='Dark'] {
--primary: theme('colors.dark-theme.yellow');
--primary: theme('colors.dark-theme.yellow.DEFAULT');
--primary-dark: theme('colors.dark-theme.yellow.dark');
--red: theme('colors.dark-theme.red.DEFAULT');
--red-dark: theme('colors.dark-theme.red.dark');
--green: theme('colors.dark-theme.green.DEFAULT');
@ -37,7 +39,8 @@
}
[data-theme='Mango'] {
--primary: theme('colors.mango-theme.yellow');
--primary: theme('colors.mango-theme.yellow.DEFAULT');
--primary-dark: theme('colors.mango-theme.yellow.dark');
--red: theme('colors.mango-theme.red.DEFAULT');
--red-dark: theme('colors.mango-theme.red.dark');
--green: theme('colors.mango-theme.green.DEFAULT');
@ -63,7 +66,7 @@ p {
}
a {
@apply text-th-primary transition-all duration-300 hover:opacity-70;
@apply text-th-primary transition-all duration-300 hover:text-th-primary-dark;
}
button {

View File

@ -49,7 +49,10 @@ module.exports = {
darkest: '#061f23',
},
'light-theme': {
orange: '#FF9C24',
orange: {
DEFAULT: '#FF9C24',
dark: '#F58700',
},
red: { DEFAULT: '#CC2929', dark: '#AA2222' },
green: { DEFAULT: '#5EBF4D', dark: '#4BA53B' },
'bkg-1': '#f7f7f7',
@ -61,7 +64,10 @@ module.exports = {
'fgd-4': '#B0B0B0',
},
'dark-theme': {
yellow: '#F2C94C',
yellow: {
DEFAULT: '#F2C94C',
dark: '#E4AF11',
},
red: { DEFAULT: '#CC2929', dark: '#AA2222' },
green: { DEFAULT: '#5EBF4D', dark: '#4BA53B' },
orange: { DEFAULT: '#FF9C24' },
@ -74,7 +80,10 @@ module.exports = {
'fgd-4': '#878787',
},
'mango-theme': {
yellow: '#F2C94C',
yellow: {
DEFAULT: '#F2C94C',
dark: '#E4AF11',
},
red: {
DEFAULT: '#E54033',
dark: '#C7251A',
@ -100,6 +109,7 @@ module.exports = {
'th-fgd-3': 'var(--fgd-3)',
'th-fgd-4': 'var(--fgd-4)',
'th-primary': 'var(--primary)',
'th-primary-dark': 'var(--primary-dark)',
'th-red': 'var(--red)',
'th-red-dark': 'var(--red-dark)',
'th-green': 'var(--green)',

View File

@ -176,6 +176,17 @@ export const tokenPrecision = {
WUSDT: 2,
}
// Precision for depositing/withdrawing
export const DECIMALS = {
BTC: 5,
ETH: 4,
SOL: 2,
SRM: 2,
USDC: 2,
USDT: 2,
WUSDT: 2,
}
export const getSymbolForTokenMintAddress = (address: string): string => {
if (address && address.length) {
return TOKEN_MINTS.find((m) => m.address.toString() === address)?.name || ''
@ -197,3 +208,11 @@ export const copyToClipboard = (copyThis) => {
document.execCommand('copy')
document.body.removeChild(el)
}
// Truncate decimals without rounding
export const trimDecimals = (n, digits) => {
var step = Math.pow(10, digits || 0)
var temp = Math.trunc(step * n)
return temp / step
}

View File

@ -1603,3 +1603,93 @@ async function packageAndSend(
successMessage,
})
}
export async function settleAllTrades(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
marginAccount: MarginAccount,
markets: Market[],
wallet: Wallet
): Promise<TransactionSignature> {
const transaction = new Transaction()
const assetGains: number[] = new Array(NUM_TOKENS).fill(0)
for (let i = 0; i < NUM_MARKETS; i++) {
const openOrdersAccount = marginAccount.openOrdersAccounts[i]
if (openOrdersAccount === undefined) {
continue
} else if (
openOrdersAccount.quoteTokenFree.toNumber() === 0 &&
openOrdersAccount.baseTokenFree.toNumber() === 0
) {
continue
}
assetGains[i] += openOrdersAccount.baseTokenFree.toNumber()
assetGains[NUM_TOKENS - 1] += openOrdersAccount.quoteTokenFree.toNumber()
const spotMarket = markets[i]
const dexSigner = await PublicKey.createProgramAddress(
[
spotMarket.publicKey.toBuffer(),
spotMarket['_decoded'].vaultSignerNonce.toArrayLike(Buffer, 'le', 8),
],
spotMarket.programId
)
const keys = [
{ isSigner: false, isWritable: true, pubkey: mangoGroup.publicKey },
{ isSigner: true, isWritable: false, pubkey: wallet.publicKey },
{ isSigner: false, isWritable: true, pubkey: marginAccount.publicKey },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
{ isSigner: false, isWritable: false, pubkey: spotMarket.programId },
{ isSigner: false, isWritable: true, pubkey: spotMarket.publicKey },
{
isSigner: false,
isWritable: true,
pubkey: marginAccount.openOrders[i],
},
{ isSigner: false, isWritable: false, pubkey: mangoGroup.signerKey },
{
isSigner: false,
isWritable: true,
pubkey: spotMarket['_decoded'].baseVault,
},
{
isSigner: false,
isWritable: true,
pubkey: spotMarket['_decoded'].quoteVault,
},
{ isSigner: false, isWritable: true, pubkey: mangoGroup.vaults[i] },
{
isSigner: false,
isWritable: true,
pubkey: mangoGroup.vaults[mangoGroup.vaults.length - 1],
},
{ isSigner: false, isWritable: false, pubkey: dexSigner },
{ isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
]
const data = encodeMangoInstruction({ SettleFunds: {} })
const settleFundsInstruction = new TransactionInstruction({
keys,
data,
programId,
})
transaction.add(settleFundsInstruction)
}
if (transaction.instructions.length === 0) {
throw new Error('No unsettled funds')
}
return await packageAndSend(
transaction,
connection,
wallet,
[],
'Settle All Trades'
)
}