Merges main

This commit is contained in:
Luc Succes 2022-03-29 12:11:23 -04:00
commit 9718bf5d38
104 changed files with 4038 additions and 4958 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_GROUP=devnet.2

View File

@ -0,0 +1,113 @@
name: 'Next.js Bundle Analysis'
on:
pull_request:
push:
branches:
- main # change this if your default branch is named differently
workflow_dispatch:
defaults:
run:
# change this if your nextjs app does not live at the root of the repo
working-directory: ./
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up node
uses: actions/setup-node@v1
with:
node-version: '14.x'
- name: Install dependencies
uses: bahmutov/npm-install@v1
- name: Restore next build
uses: actions/cache@v2
id: restore-build-cache
env:
cache-name: cache-next-build
with:
# if you use a custom build directory, replace all instances of `.next` in this file with your build directory
# ex: if your app builds to `dist`, replace `.next` with `dist`
path: .next/cache
# change this if you prefer a more strict cache
key: ${{ runner.os }}-build-${{ env.cache-name }}
- name: Build next.js app
# change this if your site requires a custom build command
run: ./node_modules/.bin/next build
# Here's the first place where next-bundle-analysis' own script is used
# This step pulls the raw bundle stats for the current bundle
- name: Analyze bundle
run: npx -p nextjs-bundle-analysis report
- name: Upload bundle
uses: actions/upload-artifact@v2
with:
name: bundle
path: .next/analyze/__bundle_analysis.json
- name: Download base branch bundle stats
uses: dawidd6/action-download-artifact@v2
if: success() && github.event.number
with:
workflow: nextjs_bundle_analysis.yml
branch: ${{ github.event.pull_request.base.ref }}
path: .next/analyze/base
# And here's the second place - this runs after we have both the current and
# base branch bundle stats, and will compare them to determine what changed.
# There are two configurable arguments that come from package.json:
#
# - budget: optional, set a budget (bytes) against which size changes are measured
# it's set to 350kb here by default, as informed by the following piece:
# https://infrequently.org/2021/03/the-performance-inequality-gap/
#
# - red-status-percentage: sets the percent size increase where you get a red
# status indicator, defaults to 20%
#
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
# entry in your package.json file.
- name: Compare with base branch bundle
if: success() && github.event.number
run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
- name: Get comment body
id: get-comment-body
if: success() && github.event.number
run: |
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find Comment
uses: peter-evans/find-comment@v1
if: success() && github.event.number
id: fc
with:
issue-number: ${{ github.event.number }}
body-includes: '<!-- __NEXTJS_BUNDLE -->'
- name: Create Comment
uses: peter-evans/create-or-update-comment@v1.4.4
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
with:
issue-number: ${{ github.event.number }}
body: ${{ steps.get-comment-body.outputs.body }}
- name: Update Comment
uses: peter-evans/create-or-update-comment@v1.4.4
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
with:
issue-number: ${{ github.event.number }}
body: ${{ steps.get-comment-body.outputs.body }}
comment-id: ${{ steps.fc.outputs.comment-id }}
edit-mode: replace

View File

@ -5,13 +5,12 @@ name: Node.js CI
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
@ -20,12 +19,12 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: yarn install --frozen-lockfile
- run: yarn type-check
- run: yarn lint
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: yarn install --frozen-lockfile
- run: yarn type-check
- run: yarn lint
- run: yarn prettier --check .

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local

View File

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged
yarn lint-all

View File

@ -1,4 +1,4 @@
import { AccountInfo, PublicKey, Transaction } from '@solana/web3.js'
import { AccountInfo, PublicKey } from '@solana/web3.js'
import { Market, OpenOrders } from '@project-serum/serum'
import { Event } from '@project-serum/serum/lib/queue'
import { I80F48 } from '@blockworks-foundation/mango-client'
@ -122,17 +122,6 @@ export const DEFAULT_PUBLIC_KEY = new PublicKey(
'11111111111111111111111111111111'
)
export interface WalletAdapter {
publicKey: PublicKey
autoApprove: boolean
connected: boolean
signTransaction: (transaction: Transaction) => Promise<Transaction>
signAllTransactions: (transaction: Transaction[]) => Promise<Transaction[]>
connect: () => any
disconnect: () => any
on(event: string, fn: () => void): this
}
export interface PerpTriggerOrder {
orderId: number
marketIndex: number

View File

@ -29,17 +29,17 @@ import { useTranslation } from 'next-i18next'
import useMangoAccount from '../hooks/useMangoAccount'
import Loading from './Loading'
import CreateAlertModal from './CreateAlertModal'
import { useWallet } from '@solana/wallet-adapter-react'
const I80F48_100 = I80F48.fromString('100')
export default function AccountInfo() {
const { t } = useTranslation('common')
const connected = useMangoStore((s) => s.wallet.connected)
const { publicKey, wallet, connected } = useWallet()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const { mangoAccount, initialLoad } = useMangoAccount()
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const wallet = useMangoStore((s) => s.wallet.current)
const actions = useMangoStore((s) => s.actions)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
@ -50,8 +50,8 @@ export default function AccountInfo() {
const [showAlertsModal, setShowAlertsModal] = useState(false)
const canWithdraw =
mangoAccount?.owner && wallet?.publicKey
? mangoAccount?.owner?.equals(wallet?.publicKey)
mangoAccount?.owner && publicKey
? mangoAccount?.owner?.equals(publicKey)
: false
const handleCloseDeposit = useCallback(() => {
@ -78,7 +78,6 @@ export default function AccountInfo() {
// console.log('rerendering account info', mangoAccount, mngoAccrued.toNumber())
const handleRedeemMngo = async () => {
const wallet = useMangoStore.getState().wallet.current
const mangoClient = useMangoStore.getState().connection.client
const mngoNodeBank =
mangoGroup.rootBankAccounts[MNGO_INDEX].nodeBankAccounts[0]
@ -88,7 +87,7 @@ export default function AccountInfo() {
const txid = await mangoClient.redeemAllMngo(
mangoGroup,
mangoAccount,
wallet,
wallet?.adapter,
mangoGroup.tokens[MNGO_INDEX].rootBank,
mngoNodeBank.publicKey,
mngoNodeBank.vault

View File

@ -11,6 +11,7 @@ import { ElementTitle } from './styles'
import Tooltip from './Tooltip'
import { notify } from '../utils/notifications'
import { useTranslation } from 'next-i18next'
import { useWallet } from '@solana/wallet-adapter-react'
interface AccountNameModalProps {
accountName?: string
@ -24,6 +25,7 @@ const AccountNameModal: FunctionComponent<AccountNameModalProps> = ({
onClose,
}) => {
const { t } = useTranslation('common')
const { wallet } = useWallet()
const [name, setName] = useState(accountName || '')
const [invalidNameMessage, setInvalidNameMessage] = useState('')
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
@ -32,16 +34,15 @@ const AccountNameModal: FunctionComponent<AccountNameModalProps> = ({
const submitName = async () => {
const mangoClient = useMangoStore.getState().connection.client
const wallet = useMangoStore.getState().wallet.current
try {
const txid = await mangoClient.addMangoAccountInfo(
mangoGroup,
mangoAccount,
wallet,
wallet?.adapter,
name
)
actions.fetchAllMangoAccounts()
actions.fetchAllMangoAccounts(wallet)
actions.reloadMangoAccount()
onClose()
notify({

View File

@ -12,6 +12,7 @@ import Button, { LinkButton } from './Button'
import NewAccount from './NewAccount'
import { useTranslation } from 'next-i18next'
import Tooltip from './Tooltip'
import { useWallet } from '@solana/wallet-adapter-react'
export const LAST_ACCOUNT_KEY = 'lastAccountViewed-3.0'
@ -25,6 +26,7 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
onClose,
}) => {
const { t } = useTranslation('common')
const { publicKey } = useWallet()
const [showNewAccountForm, setShowNewAccountForm] = useState(false)
const [newAccPublicKey, setNewAccPublicKey] = useState(null)
const mangoAccounts = useMangoStore((s) => s.mangoAccounts)
@ -35,7 +37,6 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
const setMangoStore = useMangoStore((s) => s.set)
const actions = useMangoStore((s) => s.actions)
const [, setLastAccountViewed] = useLocalStorageState(LAST_ACCOUNT_KEY)
const wallet = useMangoStore.getState().wallet.current
const handleMangoAccountChange = (mangoAccount: MangoAccount) => {
setLastAccountViewed(mangoAccount.publicKey.toString())
@ -124,9 +125,7 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
<div className="flex items-center pb-0.5">
{account?.name ||
abbreviateAddress(account.publicKey)}
{!account?.owner.equals(
wallet?.publicKey
) ? (
{!account?.owner.equals(publicKey) ? (
<Tooltip
content={t(
'delegate:delegated-account'
@ -138,14 +137,14 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
''
)}
</div>
{mangoGroup ? (
{mangoGroup && (
<div className="text-xs text-th-fgd-3">
<AccountInfo
mangoGroup={mangoGroup}
mangoAccount={account}
/>
</div>
) : null}
)}
</div>
</RadioGroup.Label>
</div>

View File

@ -9,7 +9,7 @@ import { SHOW_TOUR_KEY } from './IntroTips'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import { useRouter } from 'next/router'
import { LANGS } from './LanguageSwitch'
import { LANGS } from './SettingsModal'
import { RadioGroup } from '@headlessui/react'
export const ALPHA_MODAL_KEY = 'mangoAlphaAccepted-3.06'

View File

@ -19,6 +19,7 @@ import { useTranslation } from 'next-i18next'
import { TransactionSignature } from '@solana/web3.js'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useWallet } from '@solana/wallet-adapter-react'
const BalancesTable = ({
showZeroBalances = false,
@ -54,10 +55,8 @@ const BalancesTable = ({
const [submitting, setSubmitting] = useState(false)
const isMobile = width ? width < breakpoints.md : false
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const wallet = useMangoStore((s) => s.wallet.current)
const canWithdraw = wallet?.publicKey
? mangoAccount?.owner.equals(wallet.publicKey)
: true
const { wallet, publicKey } = useWallet()
const canWithdraw = publicKey ? mangoAccount?.owner.equals(publicKey) : true
const { asPath } = useRouter()
const handleSizeClick = (size, symbol) => {
@ -118,7 +117,7 @@ const BalancesTable = ({
mangoGroup,
mangoAccount,
spotMarkets,
wallet
wallet?.adapter
)
for (const txid of txids) {
@ -148,6 +147,24 @@ const BalancesTable = ({
const unsettledBalances = balances.filter((bal) => bal.unsettled > 0)
const trimDecimals = useCallback((num: string) => {
if (parseFloat(num) === 0) {
return '0'
}
// Trim the decimals depending on the length of the whole number
const splitNum = num.split('.')
if (splitNum.length > 1) {
const wholeNum = splitNum[0]
const decimals = splitNum[1]
if (wholeNum.length > 8) {
return `${wholeNum}.${decimals.substring(0, 2)}`
} else if (wholeNum.length > 3) {
return `${wholeNum}.${decimals.substring(0, 3)}`
}
}
return num
}, [])
return (
<div className={`flex flex-col pb-2 sm:pb-4`}>
{unsettledBalances.length > 0 ? (
@ -360,101 +377,116 @@ const BalancesTable = ({
</TrHead>
</thead>
<tbody>
{items.map((balance, index) => (
<TrBody key={`${balance.symbol}${index}`}>
<Td>
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${balance.symbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{balance.symbol === 'USDC' ||
decodeURIComponent(asPath).includes(
`${balance.symbol}/USDC`
) ? (
<span>{balance.symbol}</span>
) : (
<Link
href={{
pathname: '/',
query: { name: `${balance.symbol}/USDC` },
}}
shallow={true}
>
<a className="text-th-fgd-1 underline hover:text-th-fgd-1 hover:no-underline">
{balance.symbol}
</a>
</Link>
)}
</div>
</Td>
<Td>{balance.deposits.toFormat(balance.decimals)}</Td>
<Td>{balance.borrows.toFormat(balance.decimals)}</Td>
<Td>{balance.orders}</Td>
<Td>{balance.unsettled}</Td>
<Td>
{marketConfig.kind === 'spot' &&
marketConfig.name.includes(balance.symbol) &&
selectedMarket &&
clickToPopulateTradeForm ? (
<span
className={
balance.net.toNumber() != 0
? 'cursor-pointer underline hover:no-underline'
: ''
}
onClick={() =>
handleSizeClick(balance.net, balance.symbol)
}
>
{balance.net.toFormat(balance.decimals)}
</span>
) : (
balance.net.toFormat(balance.decimals)
)}
</Td>
<Td>{formatUsdValue(balance.value.toNumber())}</Td>
<Td>
<span className="text-th-green">
{balance.depositRate.toFixed(2)}%
</span>
</Td>
<Td>
<span className="text-th-red">
{balance.borrowRate.toFixed(2)}%
</span>
</Td>
{showDepositWithdraw ? (
{items.map((balance, index) => {
if (!balance) {
return null
}
return (
<TrBody key={`${balance.symbol}${index}`}>
<Td>
<div className="flex justify-end">
<Button
className="h-7 pt-0 pb-0 pl-3 pr-3 text-xs"
onClick={() =>
handleOpenDepositModal(balance.symbol)
}
>
{balance.borrows.toNumber() > 0
? t('repay')
: t('deposit')}
</Button>
<Button
className="ml-4 h-7 pt-0 pb-0 pl-3 pr-3 text-xs"
onClick={() =>
handleOpenWithdrawModal(balance.symbol)
}
disabled={!canWithdraw}
>
{t('withdraw')}
</Button>
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${balance.symbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{balance.symbol === 'USDC' ||
decodeURIComponent(asPath).includes(
`${balance.symbol}/USDC`
) ? (
<span>{balance.symbol}</span>
) : (
<Link
href={{
pathname: '/',
query: { name: `${balance.symbol}/USDC` },
}}
shallow={true}
>
<a className="text-th-fgd-1 underline hover:text-th-fgd-1 hover:no-underline">
{balance.symbol}
</a>
</Link>
)}
</div>
</Td>
) : null}
</TrBody>
))}
<Td>
{trimDecimals(
balance.deposits.toFormat(balance.decimals)
)}
</Td>
<Td>
{trimDecimals(
balance.borrows.toFormat(balance.decimals)
)}
</Td>
<Td>{balance.orders}</Td>
<Td>{balance.unsettled}</Td>
<Td>
{marketConfig.kind === 'spot' &&
marketConfig.name.includes(balance.symbol) &&
selectedMarket &&
clickToPopulateTradeForm ? (
<span
className={
balance.net.toNumber() != 0
? 'cursor-pointer underline hover:no-underline'
: ''
}
onClick={() =>
handleSizeClick(balance.net, balance.symbol)
}
>
{trimDecimals(
balance.net.toFormat(balance.decimals)
)}
</span>
) : (
trimDecimals(balance.net.toFormat(balance.decimals))
)}
</Td>
<Td>{formatUsdValue(balance.value.toNumber())}</Td>
<Td>
<span className="text-th-green">
{balance.depositRate.toFixed(2)}%
</span>
</Td>
<Td>
<span className="text-th-red">
{balance.borrowRate.toFixed(2)}%
</span>
</Td>
{showDepositWithdraw ? (
<Td>
<div className="flex justify-end">
<Button
className="h-7 pt-0 pb-0 pl-3 pr-3 text-xs"
onClick={() =>
handleOpenDepositModal(balance.symbol)
}
>
{balance.borrows.toNumber() > 0
? t('repay')
: t('deposit')}
</Button>
<Button
className="ml-4 h-7 pt-0 pb-0 pl-3 pr-3 text-xs"
onClick={() =>
handleOpenWithdrawModal(balance.symbol)
}
disabled={!canWithdraw}
>
{t('withdraw')}
</Button>
</div>
</Td>
) : null}
</TrBody>
)
})}
</tbody>
{showDepositModal && (
<DepositModal

View File

@ -42,7 +42,7 @@ export const LinkButton: FunctionComponent<ButtonProps> = ({
disabled={disabled}
className={`border-0 font-bold ${
primary ? 'text-th-primary' : 'text-th-fgd-2'
} underline hover:no-underline hover:opacity-60 focus:outline-none ${className}`}
} underline hover:no-underline hover:opacity-60 focus:outline-none disabled:cursor-not-allowed disabled:underline disabled:opacity-60 ${className}`}
{...props}
>
{children}

View File

@ -5,7 +5,7 @@ interface ButtonGroupProps {
className?: string
onChange: (x) => void
unit?: string
values: Array<string>
values: Array<any>
names?: Array<string>
}

View File

@ -2,7 +2,7 @@ import React from 'react'
import { CheckIcon } from '@heroicons/react/solid'
const Checkbox = ({ checked, children, disabled = false, ...props }) => (
<label className="flex cursor-pointer items-center text-th-fgd-3">
<label className="default-transition flex cursor-pointer items-center text-th-fgd-3 hover:text-th-fgd-2">
<input
checked={checked}
{...props}
@ -32,7 +32,11 @@ const Checkbox = ({ checked, children, disabled = false, ...props }) => (
}`}
/>
</div>
<span className="ml-2 text-xs">{children}</span>
<span
className={`ml-2 text-xs ${checked && !disabled ? 'text-th-fgd-2' : ''}`}
>
{children}
</span>
</label>
)

View File

@ -23,6 +23,7 @@ import {
ZERO_I80F48,
} from '@blockworks-foundation/mango-client'
import { formatUsdValue } from '../utils'
import { useWallet } from '@solana/wallet-adapter-react'
interface CloseAccountModalProps {
lamports?: number
@ -35,6 +36,7 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
onClose,
}) => {
const { t } = useTranslation(['common', 'close-account'])
const { wallet } = useWallet()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
@ -94,7 +96,6 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
}, [mangoAccount])
const closeAccount = async () => {
const wallet = useMangoStore.getState().wallet.current
const mangoClient = useMangoStore.getState().connection.client
try {
@ -103,10 +104,10 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
mangoAccount,
mangoCache,
MNGO_INDEX,
wallet
wallet?.adapter
)
await actions.fetchAllMangoAccounts()
await actions.fetchAllMangoAccounts(wallet)
const mangoAccounts = useMangoStore.getState().mangoAccounts
setMangoStore((state) => {

View File

@ -1,54 +1,107 @@
import { Fragment, useCallback, useState } from 'react'
import useMangoStore from '../stores/useMangoStore'
import React, {
Fragment,
useCallback,
useState,
useMemo,
useEffect,
} from 'react'
import { Menu, Transition } from '@headlessui/react'
import { useWallet, Wallet } from '@solana/wallet-adapter-react'
import { WalletReadyState } from '@solana/wallet-adapter-base'
import {
CurrencyDollarIcon,
DuplicateIcon,
LogoutIcon,
} from '@heroicons/react/outline'
import { PROVIDER_LOCAL_STORAGE_KEY } from '../hooks/useWallet'
import useLocalStorageState from '../hooks/useLocalStorageState'
import { abbreviateAddress, copyToClipboard } from '../utils'
import WalletSelect from './WalletSelect'
import { WalletIcon, ProfileIcon } from './icons'
import AccountsModal from './AccountsModal'
import { useEffect } from 'react'
import { notify } from 'utils/notifications'
import { abbreviateAddress, copyToClipboard } from 'utils'
import useMangoStore from 'stores/useMangoStore'
import { ProfileIcon, WalletIcon } from './icons'
import { useTranslation } from 'next-i18next'
import { DEFAULT_PROVIDER, WALLET_PROVIDERS } from '../utils/wallet-adapters'
import { WalletSelect } from 'components/WalletSelect'
import AccountsModal from './AccountsModal'
import { uniqBy } from 'lodash'
const ConnectWalletButton = () => {
const { t } = useTranslation('common')
const wallet = useMangoStore((s) => s.wallet.current)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const pfp = useMangoStore((s) => s.wallet.pfp)
const connected = useMangoStore((s) => s.wallet.connected)
const set = useMangoStore((s) => s.set)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [selectedWallet, setSelectedWallet] = useState(DEFAULT_PROVIDER.url)
const [savedProviderUrl] = useLocalStorageState(
PROVIDER_LOCAL_STORAGE_KEY,
DEFAULT_PROVIDER.url
)
// update in useEffect to prevent SRR error from next.js
useEffect(() => {
setSelectedWallet(savedProviderUrl)
}, [savedProviderUrl])
const handleWalletConect = () => {
wallet.connect()
set((state) => {
state.selectedMangoAccount.initialLoad = true
export const handleWalletConnect = (wallet: Wallet) => {
if (!wallet) {
return
}
if (wallet.readyState === WalletReadyState.NotDetected) {
window.open(wallet.adapter.url, '_blank')
} else {
wallet?.adapter?.connect().catch((e) => {
if (e.name.includes('WalletLoadError')) {
notify({
title: `${wallet.adapter.name} Error`,
type: 'error',
description: `Please install ${wallet.adapter.name} and then reload this page.`,
})
}
})
}
}
export const ConnectWalletButton: React.FC = () => {
const { connected, publicKey, wallet, wallets, select } = useWallet()
const { t } = useTranslation('common')
const pfp = useMangoStore((s) => s.wallet.pfp)
const set = useMangoStore((s) => s.set)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const installedWallets = useMemo(() => {
const installed: Wallet[] = []
for (const wallet of wallets) {
if (wallet.readyState === WalletReadyState.Installed) {
installed.push(wallet)
}
}
return installed
}, [wallets])
const displayedWallets = useMemo(() => {
return uniqBy([...installedWallets, ...wallets], (w) => {
return w.adapter.name
})
}, [wallets, installedWallets])
const handleConnect = useCallback(() => {
handleWalletConnect(wallet)
}, [wallet])
const handleCloseAccounts = useCallback(() => {
setShowAccountsModal(false)
}, [])
const handleDisconnect = useCallback(() => {
wallet?.adapter?.disconnect()
set((state) => {
state.mangoAccounts = []
state.selectedMangoAccount.current = null
state.tradeHistory = {
spot: [],
perp: [],
parsed: [],
initialLoad: false,
}
})
notify({
type: 'info',
title: t('wallet-disconnected'),
})
}, [wallet, set, t])
useEffect(() => {
if (!wallet && displayedWallets?.length) {
select(displayedWallets[0].adapter.name)
}
}, [wallet, displayedWallets, select])
return (
<>
{connected && wallet?.publicKey ? (
{connected && publicKey ? (
<Menu>
{({ open }) => (
<div className="relative" id="profile-menu-tip">
@ -83,7 +136,7 @@ const ConnectWalletButton = () => {
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => copyToClipboard(wallet?.publicKey)}
onClick={() => copyToClipboard(publicKey)}
>
<DuplicateIcon className="h-4 w-4" />
<div className="pl-2 text-left">{t('copy-address')}</div>
@ -92,13 +145,13 @@ const ConnectWalletButton = () => {
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => wallet.disconnect()}
onClick={handleDisconnect}
>
<LogoutIcon className="h-4 w-4" />
<div className="pl-2 text-left">
<div className="pb-0.5">{t('disconnect')}</div>
<div className="text-xs text-th-fgd-4">
{abbreviateAddress(wallet?.publicKey)}
{abbreviateAddress(publicKey)}
</div>
</div>
</button>
@ -114,8 +167,8 @@ const ConnectWalletButton = () => {
id="connect-wallet-tip"
>
<button
onClick={handleWalletConect}
disabled={!wallet || !mangoGroup}
onClick={handleConnect}
disabled={!mangoGroup}
className="rounded-none bg-th-primary-dark text-th-bkg-1 hover:brightness-[1.1] focus:outline-none disabled:cursor-wait disabled:text-th-bkg-2"
>
<div className="default-transition flex h-full flex-row items-center justify-center px-3">
@ -124,25 +177,25 @@ const ConnectWalletButton = () => {
<div className="mb-0.5 whitespace-nowrap font-bold">
{t('connect')}
</div>
<div className="text-xxs font-normal leading-3 tracking-wider text-th-bkg-2">
{WALLET_PROVIDERS.find((p) => p.url === selectedWallet)?.name}
</div>
{wallet?.adapter?.name && (
<div className="text-xxs font-normal leading-3 tracking-wider text-th-bkg-2">
{wallet.adapter.name}
</div>
)}
</div>
</div>
</button>
<div className="relative">
<WalletSelect />
<WalletSelect wallets={displayedWallets} />
</div>
</div>
)}
{showAccountsModal ? (
{showAccountsModal && (
<AccountsModal
onClose={handleCloseAccounts}
isOpen={showAccountsModal}
/>
) : null}
)}
</>
)
}
export default ConnectWalletButton

97
components/DatePicker.tsx Normal file
View File

@ -0,0 +1,97 @@
import DatePicker from 'react-datepicker/dist/react-datepicker'
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
import Select from './Select'
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
const MangoDatePicker = ({ date, setDate, ...props }) => {
const generateArrayOfYears = () => {
const max = new Date().getFullYear()
const min = max - (max - 2020)
const years = []
for (let i = max; i >= min; i--) {
years.push(i)
}
return years
}
const years = generateArrayOfYears()
return (
<DatePicker
renderCustomHeader={({
date,
changeYear,
changeMonth,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<div className="flex items-center justify-between px-1">
<button
className="default-transition mr-1 text-th-fgd-3 hover:text-th-fgd-1"
onClick={decreaseMonth}
disabled={prevMonthButtonDisabled}
>
<ChevronLeftIcon className="h-6 w-6" />
</button>
<div className="flex space-x-2">
<Select
className="w-28"
dropdownPanelClassName="text-left"
value={months[date.getMonth()]}
onChange={(value) => changeMonth(months.indexOf(value))}
>
{months.map((option) => (
<Select.Option key={option} value={option}>
{option}
</Select.Option>
))}
</Select>
<Select
dropdownPanelClassName="text-left"
value={date.getFullYear()}
onChange={(value) => changeYear(value)}
>
{years.map((option) => (
<Select.Option key={option} value={option}>
{option}
</Select.Option>
))}
</Select>
</div>
<button
className="default-transition ml-1 text-th-fgd-3 hover:text-th-fgd-1"
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
>
<ChevronRightIcon className="h-6 w-6" />
</button>
</div>
)}
placeholderText="dd/mm/yyyy"
dateFormat="dd/MM/yyyy"
selected={date}
onChange={(date: Date) => setDate(date)}
className="default-transition h-10 w-full cursor-pointer rounded-md border border-th-bkg-4 bg-th-bkg-1 px-2 text-th-fgd-1 hover:border-th-fgd-4 focus:border-th-fgd-4 focus:outline-none"
{...props}
/>
)
}
export default MangoDatePicker

View File

@ -13,6 +13,7 @@ import { ElementTitle } from './styles'
import { notify } from '../utils/notifications'
import { useTranslation } from 'next-i18next'
import { PublicKey } from '@solana/web3.js'
import { useWallet } from '@solana/wallet-adapter-react'
interface DelegateModalProps {
delegate?: PublicKey
@ -26,6 +27,7 @@ const DelegateModal: FunctionComponent<DelegateModalProps> = ({
onClose,
}) => {
const { t } = useTranslation(['common', 'delegate'])
const { wallet } = useWallet()
const [keyBase58, setKeyBase58] = useState(
delegate.equals(PublicKey.default) ? '' : delegate.toBase58()
@ -36,7 +38,6 @@ const DelegateModal: FunctionComponent<DelegateModalProps> = ({
const actions = useMangoStore((s) => s.actions)
const setDelegate = async () => {
const wallet = useMangoStore.getState().wallet.current
const mangoClient = useMangoStore.getState().connection.client
try {
@ -46,7 +47,7 @@ const DelegateModal: FunctionComponent<DelegateModalProps> = ({
const txid = await mangoClient.setDelegate(
mangoGroup,
mangoAccount,
wallet,
wallet?.adapter,
key
)
actions.reloadMangoAccount()
@ -70,7 +71,7 @@ const DelegateModal: FunctionComponent<DelegateModalProps> = ({
if (isKeyValid()) {
setInvalidKeyMessage('')
} else {
setInvalidKeyMessage(t('delegate:invalid-key'))
setInvalidKeyMessage(t('invalid-address'))
}
}

View File

@ -13,6 +13,7 @@ import { notify } from '../utils/notifications'
import { sleep, trimDecimals } from '../utils'
import { useTranslation } from 'next-i18next'
import ButtonGroup from './ButtonGroup'
import { useWallet } from '@solana/wallet-adapter-react'
interface DepositModalProps {
onClose: () => void
@ -28,6 +29,7 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
tokenSymbol = '',
}) => {
const { t } = useTranslation('common')
const { wallet } = useWallet()
const [inputAmount, setInputAmount] = useState<string>(repayAmount || '')
const [submitting, setSubmitting] = useState(false)
const [invalidAmountMessage, setInvalidAmountMessage] = useState('')
@ -65,6 +67,7 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
amount: parseFloat(inputAmount),
fromTokenAcc: selectedAccount.account,
mangoAccount,
wallet,
})
.then((response) => {
notify({
@ -77,7 +80,7 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
sleep(500).then(() => {
mangoAccount
? actions.reloadMangoAccount()
: actions.fetchAllMangoAccounts()
: actions.fetchAllMangoAccounts(wallet)
actions.fetchWalletTokens()
})
})

View File

@ -6,6 +6,7 @@ import Loading from './Loading'
import Modal from './Modal'
import { msrmMints } from '@blockworks-foundation/mango-client'
import { useTranslation } from 'next-i18next'
import { useWallet } from '@solana/wallet-adapter-react'
const DepositMsrmModal = ({ onClose, isOpen }) => {
const { t } = useTranslation('common')
@ -13,7 +14,7 @@ const DepositMsrmModal = ({ onClose, isOpen }) => {
const actions = useMangoStore((s) => s.actions)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const wallet = useMangoStore((s) => s.wallet.current)
const { wallet } = useWallet()
const walletTokens = useMangoStore((s) => s.wallet.tokens)
const cluster = useMangoStore.getState().connection.cluster
@ -27,7 +28,7 @@ const DepositMsrmModal = ({ onClose, isOpen }) => {
const txid = await mangoClient.depositMsrm(
mangoGroup,
mangoAccount,
wallet,
wallet?.adapter,
ownerMsrmAccount?.account?.publicKey,
1
)

View File

@ -1,5 +1,4 @@
import { FunctionComponent, ReactNode } from 'react'
import useMangoStore from '../stores/useMangoStore'
import Button from './Button'
interface EmptyStateProps {
@ -8,6 +7,7 @@ interface EmptyStateProps {
onClickButton?: () => void
desc?: string
title: string
disabled?: boolean
}
const EmptyState: FunctionComponent<EmptyStateProps> = ({
@ -16,10 +16,8 @@ const EmptyState: FunctionComponent<EmptyStateProps> = ({
onClickButton,
desc,
title,
disabled = false,
}) => {
const wallet = useMangoStore((s) => s.wallet.current)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
return (
<div className="flex flex-col items-center rounded-lg px-4 pb-2 text-th-fgd-1">
<div className="mb-1 h-6 w-6 text-th-primary">{icon}</div>
@ -34,11 +32,7 @@ const EmptyState: FunctionComponent<EmptyStateProps> = ({
</p>
) : null}
{buttonText && onClickButton ? (
<Button
className="mt-2"
onClick={onClickButton}
disabled={!wallet || !mangoGroup}
>
<Button className="mt-2" onClick={onClickButton} disabled={disabled}>
{buttonText}
</Button>
) : null}

View File

@ -1,4 +1,5 @@
import React from 'react'
import { FiveOhFive } from './FiveOhFive'
class ErrorBoundary extends React.Component<
any,
@ -39,16 +40,7 @@ class ErrorBoundary extends React.Component<
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<div className="pt-1 text-center text-th-fgd-2">
<div>Something went wrong.</div>
<div className="text-th-red">{this.state.error.message}</div>
<button className="mt-2" onClick={() => location.reload()}>
Refresh and try again
</button>
<div className="mx-8 mt-4 px-8">{this.state.error.stack}</div>
</div>
)
return <FiveOhFive error={this.state.error} />
}
return this.props.children

View File

@ -1,121 +0,0 @@
import { percentFormat } from '../utils/index'
import useSrmAccount from '../hooks/useSrmAccount'
import {
MSRM_DECIMALS,
SRM_DECIMALS,
} from '@project-serum/serum/lib/token-instructions'
import Tooltip from './Tooltip'
import { InformationCircleIcon } from '@heroicons/react/outline'
import { useTranslation } from 'next-i18next'
import Button from './Button'
import useMangoStore from '../stores/useMangoStore'
import { msrmMints, ZERO_BN } from '@blockworks-foundation/mango-client'
import DepositMsrmModal from './DepositMsrmModal'
import WithdrawMsrmModal from './WithdrawMsrmModal'
import { useState } from 'react'
const FeeDiscountsTable = () => {
const { t } = useTranslation('common')
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const connected = useMangoStore((s) => s.wallet.connected)
const walletTokens = useMangoStore((s) => s.wallet.tokens)
const { totalSrm, totalMsrm, rates } = useSrmAccount()
const [showDeposit, setShowDeposit] = useState(false)
const [showWithdraw, setShowWithdraw] = useState(false)
const cluster = useMangoStore.getState().connection.cluster
const ownerMsrmAccount = walletTokens.find((t) =>
t.account.mint.equals(msrmMints[cluster])
)
return (
<div
className={`flex justify-center divide-x divide-gray-500 rounded-md bg-th-bkg-1 py-6`}
>
<div className="pr-10">
<div className="text-center text-lg">{t('serum-fees')}</div>
<div
className={`mt-4 flex flex-col justify-between text-center text-th-fgd-4 sm:flex-row`}
>
<div className="px-4">
<div>
{totalMsrm > 0 ? 'MSRM' : 'SRM'} {t('deposits')}
</div>
<div className="text-normal text-th-fgd-3">
{totalMsrm > 0
? totalMsrm.toLocaleString(undefined, {
maximumFractionDigits: MSRM_DECIMALS,
})
: totalSrm.toLocaleString(undefined, {
maximumFractionDigits: SRM_DECIMALS,
})}
</div>
</div>
<div className="mt-4 px-4 sm:mt-0">
<div>{t('maker-fee')}</div>
<div className="text-normal text-th-fgd-3">
{rates ? percentFormat.format(rates.maker) : null}
</div>
</div>
<div className="mt-4 px-4 sm:mt-0">
<div className="flex items-center">
<div>{t('taker-fee')}</div>
<div className="flex items-center">
<Tooltip
content={t('tooltip-serum-rebate', {
taker_percent: percentFormat.format(rates.taker),
})}
>
<div>
<InformationCircleIcon
className={`ml-2 h-5 w-5 cursor-help text-th-fgd-4`}
/>
</div>
</Tooltip>
</div>
</div>
<div className="text-normal text-th-fgd-3">
{rates
? new Intl.NumberFormat(undefined, {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 3,
}).format(rates.takerWithRebate)
: null}
</div>
</div>
</div>
{connected && mangoAccount ? (
<div className="mt-6 flex justify-center">
<Button
onClick={() => setShowDeposit(true)}
disabled={!ownerMsrmAccount}
>
{t('deposit')} MSRM
</Button>
{mangoAccount.msrmAmount.gt(ZERO_BN) ? (
<Button onClick={() => setShowWithdraw(true)} className="ml-2">
{t('withdraw')} MSRM
</Button>
) : null}
</div>
) : null}
</div>
{showDeposit ? (
<DepositMsrmModal
isOpen={showDeposit}
onClose={() => setShowDeposit(false)}
/>
) : null}
{showWithdraw ? (
<WithdrawMsrmModal
isOpen={showWithdraw}
onClose={() => setShowWithdraw(false)}
/>
) : null}
</div>
)
}
export default FeeDiscountsTable

130
components/FiveOhFive.tsx Normal file
View File

@ -0,0 +1,130 @@
import { useState } from 'react'
import { ChevronRightIcon, ChevronDownIcon } from '@heroicons/react/solid'
import GradientText from './GradientText'
import cls from 'classnames'
const social = [
{
name: 'Twitter',
href: 'https://twitter.com/mangomarkets',
icon: (props) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
</svg>
),
},
{
name: 'GitHub',
href: 'https://github.com/blockworks-foundation',
icon: (props) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
),
},
]
export const FiveOhFive = ({ error }) => {
const stack = error.stack.split('\n').slice(0, 5).join('\n')
const [showDetails, toggleDetails] = useState(false)
const Icon = showDetails ? ChevronDownIcon : ChevronRightIcon
return (
<div className="bg-bg-texture flex min-h-screen flex-col bg-cover bg-bottom bg-no-repeat">
<div className="h-2 w-screen bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
<main className="my-[-2] mx-auto w-full max-w-7xl flex-grow px-4 sm:px-6 lg:px-8">
<div className="flex-shrink-0 pt-16">
<img
className="mx-auto h-12 w-auto"
src="/assets/logotext.svg"
alt="Workflow"
/>
</div>
<div className="mx-auto max-w-xl py-16 sm:py-24">
<div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide">
<GradientText>505 error</GradientText>
</p>
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-white sm:text-5xl">
Something went wrong
</h1>
<p className="mt-2 text-lg text-gray-500">
The page you are looking for could not be loaded.
</p>
</div>
<div className="my-10 text-center">
<div className="font-mono mt-8 rounded-lg bg-th-bkg-2 p-8 text-left text-th-fgd-1">
<div className="flex">
<div className="text-mango-theme-fgd-2">{error.message}</div>
<div className="flex-grow"></div>
<div className="flex-shrink-0 self-center">
<Icon
className="text-mango-yellow h-5 w-5"
aria-hidden="true"
onClick={() => toggleDetails(!showDetails)}
/>
</div>
</div>
<div
style={{
maxHeight: showDetails ? '500px' : 0,
opacity: showDetails ? 1 : 0,
}}
className={cls('overflow-hidden transition-all')}
>
<div className="mt-6">{stack}</div>
</div>
</div>
<div className="flex flex-col items-center">
<div className="mt-10 flex flex-row">
<button
className="mx-2 whitespace-nowrap rounded-full bg-th-bkg-button px-6 py-2 font-bold text-th-fgd-1 hover:brightness-[1.1] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100"
onClick={() => location.reload()}
>
Refresh and try again
</button>
<a
className="whitespace-nowrap rounded-full bg-mango-theme-bkg-3 px-6 py-2 font-bold text-th-fgd-1 hover:brightness-[1.1] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100"
href="https://discord.gg/mangomarkets"
>
<div className="flex">
<img
className="mr-2 h-[20px] w-[20px]"
src="/assets/icons/discord.svg"
/>
Join Discord
</div>
</a>
</div>
</div>
</div>
</div>
</main>
<footer className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="border-t border-gray-200 py-10 text-center md:flex md:justify-between">
<div className="mt-6 flex justify-center space-x-8 md:mt-0">
{social.map((item, itemIdx) => (
<a
key={itemIdx}
href={item.href}
className="inline-flex text-gray-400 hover:text-gray-500"
>
<span className="sr-only">{item.name}</span>
<item.icon className="h-6 w-6" aria-hidden="true" />
</a>
))}
</div>
</div>
</footer>
<div className="h-2 w-screen bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
</div>
)
}

View File

@ -1,31 +1,11 @@
import React, { FunctionComponent } from 'react'
// import styled from '@emotion/styled'
import React, { FunctionComponent, useCallback } from 'react'
import { LinkIcon } from '@heroicons/react/outline'
import useMangoStore from '../stores/useMangoStore'
import { MoveIcon } from './icons'
import EmptyState from './EmptyState'
import { useTranslation } from 'next-i18next'
// const StyledDragWrapperContent = styled.div`
// transition: all 0.25s ease-in;
// opacity: 0;
// `
// const StyledDragBkg = styled.div`
// transition: all 0.25s ease-in;
// opacity: 0;
// `
// const StyledDragWrapper = styled.div`
// :hover {
// ${StyledDragWrapperContent} {
// opacity: 1;
// }
// ${StyledDragBkg} {
// opacity: 0.9;
// }
// }
// `
import { handleWalletConnect } from 'components/ConnectWalletButton'
import { useWallet } from '@solana/wallet-adapter-react'
interface FloatingElementProps {
className?: string
@ -38,9 +18,14 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
showConnect,
}) => {
const { t } = useTranslation('common')
const { wallet, connected } = useWallet()
const { uiLocked } = useMangoStore((s) => s.settings)
const connected = useMangoStore((s) => s.wallet.connected)
const wallet = useMangoStore((s) => s.wallet.current)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const handleConnect = useCallback(() => {
handleWalletConnect(wallet)
}, [wallet])
return (
<div
className={`thin-scroll relative overflow-auto overflow-x-hidden rounded-lg bg-th-bkg-2 p-2.5 md:p-4 ${className}`}
@ -49,9 +34,10 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
<div className="absolute top-0 left-0 z-10 h-full w-full">
<div className="relative z-10 flex h-full flex-col items-center justify-center">
<EmptyState
disabled={!wallet || !mangoGroup}
buttonText={t('connect')}
icon={<LinkIcon />}
onClickButton={() => (wallet ? wallet.connect() : null)}
onClickButton={handleConnect}
title={t('connect-wallet')}
/>
</div>

View File

@ -0,0 +1,7 @@
const GradientText = (props) => (
<span className="bg-gradient-to-bl from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark bg-clip-text text-transparent">
{props.children}
</span>
)
export default GradientText

View File

@ -52,30 +52,6 @@ class IntroTips extends Component<Props, State> {
highlightClass: 'intro-highlight',
disableInteraction: true,
},
{
element: '#themes-tip',
intro: (
<div>
<h4>{this.props.t('themes-tip-title')}</h4>
<p>{this.props.t('themes-tip-desc')}</p>
</div>
),
tooltipClass: 'intro-tooltip',
highlightClass: 'intro-highlight',
disableInteraction: true,
},
{
element: '#languages-tip',
intro: (
<div>
<h4>{this.props.t('languages-tip-title')}</h4>
<p>{this.props.t('languages-tip-desc')}</p>
</div>
),
tooltipClass: 'intro-tooltip',
highlightClass: 'intro-highlight',
disableInteraction: true,
},
{
element: '#data-refresh-tip',
intro: (

View File

@ -9,11 +9,7 @@ import { useJupiter, RouteInfo } from '@jup-ag/react-hook'
import { TOKEN_LIST_URL } from '@jup-ag/core'
import { PublicKey } from '@solana/web3.js'
import useMangoStore from '../stores/useMangoStore'
import {
connectionSelector,
walletConnectedSelector,
walletSelector,
} from '../stores/selectors'
import { connectionSelector } from '../stores/selectors'
import { sortBy, sum } from 'lodash'
import {
CogIcon,
@ -46,6 +42,8 @@ import { numberFormatter } from './SwapTokenInfo'
import { useTranslation } from 'next-i18next'
import Tabs from './Tabs'
import SwapTokenInsights from './SwapTokenInsights'
import { useWallet } from '@solana/wallet-adapter-react'
import { handleWalletConnect } from 'components/ConnectWalletButton'
const TABS = ['Market Data', 'Performance Insights']
@ -53,10 +51,9 @@ type UseJupiterProps = Parameters<typeof useJupiter>[0]
const JupiterForm: FunctionComponent = () => {
const { t } = useTranslation(['common', 'swap'])
const wallet = useMangoStore(walletSelector)
const { wallet, publicKey, connected, signAllTransactions, signTransaction } =
useWallet()
const connection = useMangoStore(connectionSelector)
const connected = useMangoStore(walletConnectedSelector)
const [showSettings, setShowSettings] = useState(false)
const [depositAndFee, setDepositAndFee] = useState(null)
const [selectedRoute, setSelectedRoute] = useState<RouteInfo>(null)
@ -90,10 +87,13 @@ const JupiterForm: FunctionComponent = () => {
}
const fetchWalletTokens = useCallback(async () => {
if (!publicKey) {
return
}
const ownedTokens = []
const ownedTokenAccounts = await getTokenAccountsByOwnerWithWrappedSol(
connection,
wallet.publicKey
publicKey
)
ownedTokenAccounts.forEach((account) => {
@ -104,9 +104,8 @@ const JupiterForm: FunctionComponent = () => {
const uiBalance = nativeToUi(account.amount, decimals || 6)
ownedTokens.push({ account, uiBalance })
})
console.log('ownedToknes', ownedTokens)
setWalletTokens(ownedTokens)
}, [wallet, connection, tokens])
}, [publicKey, connection, tokens])
// @ts-ignore
const [inputTokenInfo, outputTokenInfo] = useMemo(() => {
@ -237,6 +236,10 @@ const JupiterForm: FunctionComponent = () => {
}
}, [routeMap, tokens, formValue.inputMint])
const handleConnect = useCallback(() => {
handleWalletConnect(wallet)
}, [wallet])
const inputWalletBalance = () => {
if (walletTokens.length) {
const walletToken = walletTokens.filter((t) => {
@ -352,7 +355,7 @@ const JupiterForm: FunctionComponent = () => {
<div className="col-span-12 xl:col-span-10 xl:col-start-2 ">
<div className="flex flex-col md:flex-row md:space-x-6">
<div className="w-full md:w-1/2 lg:w-1/3">
<div className="relative z-10">
<div className="relative">
{connected &&
walletTokensWithInfos.length &&
walletTokenPrices &&
@ -373,11 +376,11 @@ const JupiterForm: FunctionComponent = () => {
</div>
<a
className="flex items-center text-xs text-th-fgd-3 hover:text-th-fgd-2"
href={`https://explorer.solana.com/address/${wallet?.publicKey}`}
href={`https://explorer.solana.com/address/${publicKey}`}
target="_blank"
rel="noopener noreferrer"
>
{abbreviateAddress(wallet.publicKey)}
{abbreviateAddress(publicKey)}
<ExternalLinkIcon className="ml-0.5 -mt-0.5 h-3.5 w-3.5" />
</a>
</div>
@ -945,16 +948,21 @@ const JupiterForm: FunctionComponent = () => {
<Button
disabled={swapDisabled}
onClick={async () => {
if (!connected && zeroKey !== wallet?.publicKey) {
wallet.connect()
if (!connected && zeroKey !== publicKey) {
handleConnect()
} else if (!loading && selectedRoute && connected) {
setSwapping(true)
let txCount = 1
let errorTxid
const swapResult = await exchange({
wallet: wallet as any,
route: selectedRoute,
confirmationWaiterFactory: async (txid, totalTxs) => {
wallet: {
sendTransaction: wallet?.adapter?.sendTransaction,
publicKey: wallet?.adapter?.publicKey,
signAllTransactions,
signTransaction,
},
routeInfo: selectedRoute,
onTransaction: async (txid, totalTxs) => {
console.log('txid, totalTxs', txid, totalTxs)
if (txCount === totalTxs) {
errorTxid = txid

View File

@ -1,59 +0,0 @@
import { useEffect, useState } from 'react'
import { TranslateIcon } from '@heroicons/react/outline'
import DropMenu from './DropMenu'
import { useRouter } from 'next/router'
import dayjs from 'dayjs'
import useLocalStorageState from '../hooks/useLocalStorageState'
require('dayjs/locale/en')
require('dayjs/locale/es')
require('dayjs/locale/zh')
require('dayjs/locale/zh-tw')
export const LANGS = [
{ locale: 'en', name: 'english', description: 'english' },
{ locale: 'es', name: 'spanish', description: 'spanish' },
{
locale: 'zh_tw',
name: 'chinese-traditional',
description: 'traditional chinese',
},
{ locale: 'zh', name: 'chinese', description: 'simplified chinese' },
]
const LanguageSwitch = () => {
const router = useRouter()
const { pathname, asPath, query } = router
const [mounted, setMounted] = useState(false)
const [savedLanguage, setSavedLanguage] = useLocalStorageState('language', '')
// When mounted on client, now we can show the UI
useEffect(() => setMounted(true), [])
const handleLangChange = (e) => {
setSavedLanguage(e)
router.push({ pathname, query }, asPath, { locale: e })
dayjs.locale(e == 'zh_tw' ? 'zh-tw' : e)
}
return (
<div id="languages-tip">
{mounted ? (
<DropMenu
button={
<div className="default-transition flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 hover:text-th-primary focus:outline-none">
<TranslateIcon className="h-4 w-4" />
</div>
}
value={savedLanguage}
onChange={(lang) => handleLangChange(lang)}
options={LANGS}
/>
) : (
<div className="h-8 w-8 rounded-full bg-th-bkg-3" />
)}
</div>
)
}
export default LanguageSwitch

View File

@ -6,9 +6,11 @@ import { nativeI80F48ToUi } from '@blockworks-foundation/mango-client'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import { useTranslation } from 'next-i18next'
import { useWallet } from '@solana/wallet-adapter-react'
export default function MarketBalances() {
const { t } = useTranslation('common')
const { connected } = useWallet()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const mangoGroupCache = useMangoStore((s) => s.selectedMangoGroup.cache)
@ -17,7 +19,6 @@ export default function MarketBalances() {
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const setMangoStore = useMangoStore((s) => s.set)
const price = useMangoStore((s) => s.tradeForm.price)
const connected = useMangoStore((s) => s.wallet.connected)
const isLoading = useMangoStore((s) => s.selectedMangoAccount.initialLoad)
const baseSymbol = marketConfig.baseSymbol
const { width } = useViewport()

View File

@ -7,6 +7,7 @@ import Loading from './Loading'
import { sleep } from '../utils'
import Modal from './Modal'
import { useTranslation } from 'next-i18next'
import { useWallet } from '@solana/wallet-adapter-react'
interface MarketCloseModalProps {
onClose: () => void
@ -23,6 +24,7 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
}) => {
const { t } = useTranslation('common')
const [submitting, setSubmitting] = useState(false)
const { wallet } = useWallet()
const actions = useMangoStore((s) => s.actions)
const config = useMangoStore.getState().selectedMarket.config
@ -35,7 +37,6 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
useMangoStore.getState().accountInfos[marketConfig.asksKey.toString()]
const bidInfo =
useMangoStore.getState().accountInfos[marketConfig.bidsKey.toString()]
const wallet = useMangoStore.getState().wallet.current
const orderbook = useMangoStore.getState().selectedMarket.orderBook
const markPrice = useMangoStore.getState().selectedMarket.markPrice
@ -63,7 +64,7 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
mangoGroup,
mangoAccount,
market,
wallet,
wallet?.adapter,
side,
referencePrice * (1 + (side === 'buy' ? 1 : -1) * maxSlippage),
size,

View File

@ -14,6 +14,7 @@ import { breakpoints } from './TradePageGrid'
import { useTranslation } from 'next-i18next'
import SwitchMarketDropdown from './SwitchMarketDropdown'
import Tooltip from './Tooltip'
import { useWallet } from '@solana/wallet-adapter-react'
const OraclePrice = () => {
const oraclePrice = useOraclePrice()
@ -38,12 +39,12 @@ const OraclePrice = () => {
const MarketDetails = () => {
const { t } = useTranslation('common')
const { connected } = useWallet()
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const baseSymbol = marketConfig.baseSymbol
const selectedMarketName = marketConfig.name
const isPerpMarket = marketConfig.kind === 'perp'
const connected = useMangoStore((s) => s.wallet.connected)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
@ -105,11 +106,11 @@ const MarketDetails = () => {
{t('average-funding')}
</div>
<div className="text-th-fgd-1 md:text-xs">
{`${market?.funding1h.toLocaleString(undefined, {
maximumSignificantDigits: 3,
})}% (${(market?.funding1h * 24 * 365).toFixed(
2
)}% APR)`}
{`${market?.funding1h.toFixed(4)}% (${(
market?.funding1h *
24 *
365
).toFixed(2)}% APR)`}
</div>
</div>
</Tooltip>

View File

@ -38,6 +38,12 @@ const MarketNavItem: FunctionComponent<MarketNavItemProps> = ({
mangoGroupConfig,
market.baseSymbol
)
// The following if statement is for markets not on devnet
if (!mangoGroup.spotMarkets[marketIndex]) {
return 1
}
const ws = getWeights(mangoGroup, marketIndex, 'Init')
const w = market.name.includes('PERP')
? ws.perpAssetWeight
@ -85,7 +91,7 @@ const MarketNavItem: FunctionComponent<MarketNavItemProps> = ({
</div>
) : null}
</button>
<div className="ml-2">
<div className="ml-1">
<FavoriteMarketButton market={market} />
</div>
</div>

View File

@ -20,17 +20,62 @@ import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import { useTranslation } from 'next-i18next'
import useMangoAccount from '../hooks/useMangoAccount'
import { useWallet, Wallet } from '@solana/wallet-adapter-react'
export const settlePosPnl = async (
perpMarkets: PerpMarket[],
perpAccount: PerpAccount,
t,
mangoAccounts: MangoAccount[] | undefined,
wallet: Wallet
) => {
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const mangoCache = useMangoStore.getState().selectedMangoGroup.cache
const actions = useMangoStore.getState().actions
const mangoClient = useMangoStore.getState().connection.client
try {
const txids = await mangoClient.settlePosPnl(
mangoGroup,
mangoCache,
mangoAccount,
perpMarkets,
mangoGroup.rootBankAccounts[QUOTE_INDEX],
wallet?.adapter,
mangoAccounts
)
actions.reloadMangoAccount()
for (const txid of txids) {
if (txid) {
notify({
title: t('pnl-success'),
description: '',
txid,
})
}
}
} catch (e) {
console.log('Error settling PNL: ', `${e}`, `${perpAccount}`)
notify({
title: t('pnl-error'),
description: e.message,
txid: e.txid,
type: 'error',
})
}
}
export const settlePnl = async (
perpMarket: PerpMarket,
perpAccount: PerpAccount,
t,
mangoAccounts: MangoAccount[] | undefined
mangoAccounts: MangoAccount[] | undefined,
wallet: Wallet
) => {
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const mangoCache = useMangoStore.getState().selectedMangoGroup.cache
const wallet = useMangoStore.getState().wallet.current
const actions = useMangoStore.getState().actions
const marketIndex = mangoGroup.getPerpMarketIndex(perpMarket.publicKey)
const mangoClient = useMangoStore.getState().connection.client
@ -43,7 +88,7 @@ export const settlePnl = async (
perpMarket,
mangoGroup.rootBankAccounts[QUOTE_INDEX],
mangoCache.priceCache[marketIndex].price,
wallet,
wallet?.adapter,
mangoAccounts
)
actions.reloadMangoAccount()
@ -65,13 +110,13 @@ export const settlePnl = async (
export default function MarketPosition() {
const { t } = useTranslation('common')
const { wallet, connected } = useWallet()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const { mangoAccount, initialLoad } = useMangoAccount()
const selectedMarket = useMangoStore((s) => s.selectedMarket.current)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const connected = useMangoStore((s) => s.wallet.connected)
const setMangoStore = useMangoStore((s) => s.set)
const price = useMangoStore((s) => s.tradeForm.price)
const perpAccounts = useMangoStore((s) => s.selectedMangoAccount.perpAccounts)
@ -112,7 +157,7 @@ export default function MarketPosition() {
const handleSettlePnl = (perpMarket, perpAccount) => {
setSettling(true)
settlePnl(perpMarket, perpAccount, t, undefined).then(() => {
settlePnl(perpMarket, perpAccount, t, undefined, wallet).then(() => {
setSettling(false)
})
}
@ -248,23 +293,23 @@ export default function MarketPosition() {
)}
</div>
</div>
{basePosition ? (
{basePosition && (
<Button
onClick={() => setShowMarketCloseModal(true)}
className="mt-2.5 w-full"
>
<span>{t('market-close')}</span>
</Button>
) : null}
)}
</div>
{showMarketCloseModal ? (
{showMarketCloseModal && (
<MarketCloseModal
isOpen={showMarketCloseModal}
onClose={handleCloseWarning}
market={selectedMarket}
marketIndex={marketIndex}
/>
) : null}
)}
</>
)
}

View File

@ -21,6 +21,7 @@ import { useTranslation } from 'next-i18next'
import ButtonGroup from './ButtonGroup'
import InlineNotification from './InlineNotification'
import Modal from './Modal'
import { useWallet } from '@solana/wallet-adapter-react'
interface NewAccountProps {
onAccountCreation?: (x?) => void
@ -36,6 +37,7 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
const [depositPercentage, setDepositPercentage] = useState('')
const [invalidNameMessage, setInvalidNameMessage] = useState('')
const [name, setName] = useState('')
const { wallet } = useWallet()
const walletTokens = useMangoStore((s) => s.wallet.tokens)
const actions = useMangoStore((s) => s.actions)
@ -58,11 +60,12 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
amount: parseFloat(inputAmount),
fromTokenAcc: selectedAccount.account,
accountName: name,
wallet,
})
.then(async (response) => {
await sleep(1000)
actions.fetchWalletTokens()
actions.fetchAllMangoAccounts()
actions.fetchWalletTokens(wallet)
actions.fetchAllMangoAccounts(wallet)
setSubmitting(false)
onAccountCreation(response[0])
notify({

View File

@ -21,6 +21,7 @@ import { Row } from './TableElements'
import { PerpTriggerOrder } from '../@types/types'
import { useTranslation } from 'next-i18next'
import Input, { Label } from './Input'
import { useWallet, Wallet } from '@solana/wallet-adapter-react'
const DesktopTable = ({
cancelledOrderId,
@ -33,6 +34,7 @@ const DesktopTable = ({
}) => {
const { t } = useTranslation('common')
const { asPath } = useRouter()
const { wallet } = useWallet()
const [modifiedOrderSize, setModifiedOrderSize] = useState('')
const [modifiedOrderPrice, setModifiedOrderPrice] = useState('')
@ -170,7 +172,8 @@ const DesktopTable = ({
order,
market.account,
modifiedOrderPrice || order.price,
modifiedOrderSize || order.size
modifiedOrderSize || order.size,
wallet
)
}
>
@ -208,6 +211,7 @@ const MobileTable = ({
setEditOrderIndex,
}) => {
const { t } = useTranslation('common')
const { wallet } = useWallet()
const [modifiedOrderSize, setModifiedOrderSize] = useState('')
const [modifiedOrderPrice, setModifiedOrderPrice] = useState('')
@ -313,7 +317,8 @@ const MobileTable = ({
order,
market.account,
modifiedOrderPrice || order.price,
modifiedOrderSize || order.size
modifiedOrderSize || order.size,
wallet
)
}
>
@ -335,6 +340,7 @@ const MobileTable = ({
const OpenOrdersTable = () => {
const { t } = useTranslation('common')
const { asPath } = useRouter()
const { wallet } = useWallet()
const openOrders = useMangoStore((s) => s.selectedMangoAccount.openOrders)
const [cancelId, setCancelId] = useState(null)
const [modifyId, setModifyId] = useState(null)
@ -347,7 +353,6 @@ const OpenOrdersTable = () => {
order: Order | PerpOrder | PerpTriggerOrder,
market: Market | PerpMarket
) => {
const wallet = useMangoStore.getState().wallet.current
const selectedMangoGroup =
useMangoStore.getState().selectedMangoGroup.current
const selectedMangoAccount =
@ -361,7 +366,7 @@ const OpenOrdersTable = () => {
txid = await mangoClient.cancelSpotOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet,
wallet?.adapter,
market,
order as Order
)
@ -372,7 +377,7 @@ const OpenOrdersTable = () => {
txid = await mangoClient.removeAdvancedOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet,
wallet?.adapter,
(order as PerpTriggerOrder).orderId
)
actions.reloadOrders()
@ -380,7 +385,7 @@ const OpenOrdersTable = () => {
txid = await mangoClient.cancelPerpOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet,
wallet?.adapter,
market,
order as PerpOrder,
false
@ -407,7 +412,8 @@ const OpenOrdersTable = () => {
order: Order | PerpOrder,
market: Market | PerpMarket,
price: number,
size: number
size: number,
wallet: Wallet
) => {
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
@ -417,7 +423,6 @@ const OpenOrdersTable = () => {
useMangoStore.getState().accountInfos[marketConfig.asksKey.toString()]
const bidInfo =
useMangoStore.getState().accountInfos[marketConfig.bidsKey.toString()]
const wallet = useMangoStore.getState().wallet.current
const referrerPk = useMangoStore.getState().referrerPk
if (!wallet || !mangoGroup || !mangoAccount || !market) return
@ -440,7 +445,7 @@ const OpenOrdersTable = () => {
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
wallet?.adapter,
order as Order,
order.side,
orderPrice,
@ -453,7 +458,7 @@ const OpenOrdersTable = () => {
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
wallet?.adapter,
order as PerpOrder,
order.side,
orderPrice,

View File

@ -0,0 +1,123 @@
import { ChevronDownIcon } from '@heroicons/react/solid'
import React, { Fragment, useState } from 'react'
import { settlePnl, settlePosPnl } from 'components/MarketPosition'
import Button from 'components/Button'
import { Transition } from '@headlessui/react'
import { useTranslation } from 'next-i18next'
import Loading from 'components/Loading'
import useMangoStore from 'stores/useMangoStore'
import { useWallet } from '@solana/wallet-adapter-react'
const MenuButton: React.FC<{
onClick: () => void
text: string
disabled?: boolean
}> = ({ onClick, text, disabled }) => {
return (
<div
className={`default-transition flex items-center justify-end whitespace-nowrap pb-2.5 text-xs tracking-wider hover:cursor-pointer hover:text-th-primary ${
disabled ? 'pointer-events-none text-th-fgd-4' : 'text-th-fgd-1'
}`}
onClick={disabled ? null : onClick}
>
{text}
</div>
)
}
export const RedeemDropdown: React.FC = () => {
const { t } = useTranslation('common')
const { reloadMangoAccount } = useMangoStore((s) => s.actions)
const [settling, setSettling] = useState(false)
const { wallet } = useWallet()
const [settlingPosPnl, setSettlingPosPnl] = useState(false)
const [open, setOpen] = useState(false)
const unsettledPositions =
useMangoStore.getState().selectedMangoAccount.unsettledPerpPositions
const unsettledPositivePositions = useMangoStore
.getState()
.selectedMangoAccount.unsettledPerpPositions?.filter(
(p) => p.unsettledPnl > 0
)
const loading = settling || settlingPosPnl
const handleSettleAll = async () => {
setOpen(false)
setSettling(true)
for (const p of unsettledPositions) {
await settlePnl(p.perpMarket, p.perpAccount, t, undefined, wallet)
}
reloadMangoAccount()
setSettling(false)
}
const handleSettlePosPnl = async () => {
setOpen(false)
setSettlingPosPnl(true)
for (const p of unsettledPositivePositions) {
await settlePosPnl([p.perpMarket], p.perpAccount, t, null, wallet)
}
setSettlingPosPnl(false)
}
const buttons = [
{ onClick: handleSettleAll, disabled: false, text: t('redeem-all') },
{
onClick: handleSettlePosPnl,
disabled: !unsettledPositivePositions?.length,
text: t('redeem-positive'),
},
]
return (
<div
className="relative"
onMouseOver={() => setOpen(true)}
onMouseOut={() => setOpen(false)}
>
<Button
className="flex h-8 w-full items-center justify-center rounded-full bg-th-bkg-button pt-0 pb-0 pl-3 pr-2 text-xs font-bold hover:brightness-[1.1] hover:filter sm:w-auto"
disabled={!unsettledPositions?.length}
>
{loading ? (
<Loading />
) : (
<>
{t('redeem-pnl')}
<ChevronDownIcon
className={`default-transition h-5 w-5 ${
open ? 'rotate-180 transform' : 'rotate-360 transform'
}`}
/>
</>
)}
</Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition-all ease-in duration-200"
enterFrom="opacity-0 transform scale-75"
enterTo="opacity-100 transform scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute right-0 rounded-md bg-th-bkg-3 px-4 pt-2.5">
{buttons.map((b) => {
return (
<MenuButton
key={b.text}
onClick={b.onClick}
text={b.text}
disabled={b.disabled}
/>
)
})}
</div>
</Transition>
</div>
)
}

View File

@ -0,0 +1 @@
export * from './RedeemDropdown'

View File

@ -1,11 +1,11 @@
import { useCallback, useState } from 'react'
import React, { useCallback, useState } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import { useTranslation } from 'next-i18next'
import { ExclamationIcon } from '@heroicons/react/outline'
import useMangoStore from '../stores/useMangoStore'
import Button, { LinkButton } from '../components/Button'
import { LinkButton } from '../components/Button'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import { ExpandableRow, Table, Td, Th, TrBody, TrHead } from './TableElements'
@ -19,17 +19,18 @@ import MobileTableHeader from './mobile/MobileTableHeader'
import ShareModal from './ShareModal'
import { TwitterIcon } from './icons'
import { marketSelector } from '../stores/selectors'
import { useWallet } from '@solana/wallet-adapter-react'
import { RedeemDropdown } from 'components/PerpPositions'
const PositionsTable = () => {
const PositionsTable: React.FC = () => {
const { t } = useTranslation('common')
const { reloadMangoAccount } = useMangoStore((s) => s.actions)
const [settling, setSettling] = useState(false)
const [showShareModal, setShowShareModal] = useState(false)
const [showMarketCloseModal, setShowMarketCloseModal] = useState(false)
const [positionToShare, setPositionToShare] = useState(null)
const [settleSinglePos, setSettleSinglePos] = useState(null)
const market = useMangoStore(marketSelector)
const { wallet } = useWallet()
const price = useMangoStore((s) => s.tradeForm.price)
const setMangoStore = useMangoStore((s) => s.set)
const openPositions = useMangoStore(
@ -57,16 +58,6 @@ const PositionsTable = () => {
})
}
const handleSettleAll = async () => {
setSettling(true)
for (const p of unsettledPositions) {
await settlePnl(p.perpMarket, p.perpAccount, t, undefined)
}
reloadMangoAccount()
setSettling(false)
}
const handleCloseShare = useCallback(() => {
setShowShareModal(false)
}, [])
@ -78,7 +69,7 @@ const PositionsTable = () => {
const handleSettlePnl = async (perpMarket, perpAccount, index) => {
setSettleSinglePos(index)
await settlePnl(perpMarket, perpAccount, t, undefined)
await settlePnl(perpMarket, perpAccount, t, undefined, wallet)
setSettleSinglePos(null)
}
@ -92,12 +83,7 @@ const PositionsTable = () => {
<h3>{t('unsettled-positions')}</h3>
</div>
<Button
className="h-8 whitespace-nowrap pt-0 pb-0 pl-3 pr-3 text-xs"
onClick={handleSettleAll}
>
{settling ? <Loading /> : t('redeem-all')}
</Button>
<RedeemDropdown />
</div>
<div className="grid grid-flow-row grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{unsettledPositions.map((p, index) => {

View File

@ -6,6 +6,7 @@ const Select = ({
onChange,
children,
className = '',
dropdownPanelClassName = '',
placeholder = '',
disabled = false,
}) => {
@ -32,7 +33,7 @@ const Select = ({
{open ? (
<Listbox.Options
static
className={`thin-scroll absolute left-0 z-20 mt-1 max-h-60 w-full origin-top-left overflow-auto rounded-md bg-th-bkg-3 p-2 text-th-fgd-1 outline-none`}
className={`thin-scroll absolute left-0 z-20 mt-1 max-h-60 w-full origin-top-left overflow-auto rounded-md bg-th-bkg-3 p-2 text-th-fgd-1 outline-none ${dropdownPanelClassName}`}
>
{children}
</Listbox.Options>

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import Modal from './Modal'
@ -11,6 +11,15 @@ import Select from './Select'
import { useTranslation } from 'next-i18next'
import Switch from './Switch'
import { MarketKind } from '@blockworks-foundation/mango-client'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import ButtonGroup from './ButtonGroup'
import dayjs from 'dayjs'
require('dayjs/locale/en')
require('dayjs/locale/es')
require('dayjs/locale/zh')
require('dayjs/locale/zh-tw')
const NODE_URLS = [
{ label: 'Triton (RPC Pool)', value: 'https://mango.rpcpool.com' },
@ -18,9 +27,26 @@ const NODE_URLS = [
label: 'Genesys Go',
value: 'https://mango.genesysgo.net/',
},
{
label: 'Project Serum',
value: 'https://solana-api.projectserum.com/',
},
{ label: 'Custom', value: '' },
]
const THEMES = ['Light', 'Dark', 'Mango']
export const LANGS = [
{ locale: 'en', name: 'english', description: 'english' },
{ locale: 'es', name: 'spanish', description: 'spanish' },
{
locale: 'zh_tw',
name: 'chinese-traditional',
description: 'traditional chinese',
},
{ locale: 'zh', name: 'chinese', description: 'simplified chinese' },
]
const CUSTOM_NODE = NODE_URLS.find((n) => n.label === 'Custom')
export const NODE_URL_KEY = 'node-url-key-0.6'
@ -37,6 +63,8 @@ export const initialMarket = {
const SettingsModal = ({ isOpen, onClose }) => {
const { t } = useTranslation('common')
const [settingsView, setSettingsView] = useState('')
const { theme } = useTheme()
const [savedLanguage] = useLocalStorageState('language', '')
const [rpcEndpointUrl] = useLocalStorageState(
NODE_URL_KEY,
NODE_URLS[0].value
@ -58,6 +86,12 @@ const SettingsModal = ({ isOpen, onClose }) => {
const rpcEndpoint =
NODE_URLS.find((node) => node.value === rpcEndpointUrl) || CUSTOM_NODE
const savedLanguageName = useMemo(
() => LANGS.find((l) => l.locale === savedLanguage).name,
[savedLanguage]
)
return (
<Modal isOpen={isOpen} onClose={onClose}>
{settingsView !== '' ? (
@ -75,7 +109,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
{!settingsView ? (
<div className="border-b border-th-bkg-4">
<button
className="default-transition flex w-full items-center justify-between border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
onClick={() => setSettingsView('Default Market')}
>
<span>{t('default-market')}</span>
@ -85,7 +119,27 @@ const SettingsModal = ({ isOpen, onClose }) => {
</div>
</button>
<button
className="default-transition flex w-full items-center justify-between border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
onClick={() => setSettingsView('Theme')}
>
<span>{t('theme')}</span>
<div className="flex items-center text-xs text-th-fgd-3">
{theme}
<ChevronRightIcon className="ml-1 h-5 w-5 text-th-fgd-1" />
</div>
</button>
<button
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
onClick={() => setSettingsView('Language')}
>
<span>{t('language')}</span>
<div className="flex items-center text-xs text-th-fgd-3">
{t(savedLanguageName)}
<ChevronRightIcon className="ml-1 h-5 w-5 text-th-fgd-1" />
</div>
</button>
<button
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
onClick={() => setSettingsView('RPC Endpoint')}
>
<span>{t('rpc-endpoint')}</span>
@ -132,6 +186,10 @@ const SettingsContent = ({ settingsView, setSettingsView }) => {
return <DefaultMarketSettings setSettingsView={setSettingsView} />
case 'RPC Endpoint':
return <RpcEndpointSettings setSettingsView={setSettingsView} />
case 'Theme':
return <ThemeSettings setSettingsView={setSettingsView} />
case 'Language':
return <LanguageSettings />
case '':
return null
}
@ -240,3 +298,49 @@ const RpcEndpointSettings = ({ setSettingsView }) => {
</div>
)
}
const ThemeSettings = ({ setSettingsView }) => {
const { theme, setTheme } = useTheme()
const { t } = useTranslation('common')
return (
<>
<Label>{t('theme')}</Label>
<ButtonGroup
activeValue={theme}
onChange={(t) => setTheme(t)}
values={THEMES}
/>
<Button onClick={() => setSettingsView('')} className="mt-6 w-full">
<div className={`flex items-center justify-center`}>{t('save')}</div>
</Button>
</>
)
}
const LanguageSettings = () => {
const [savedLanguage, setSavedLanguage] = useLocalStorageState('language', '')
const router = useRouter()
const { pathname, asPath, query } = router
const { t } = useTranslation('common')
const handleLangChange = () => {
router.push({ pathname, query }, asPath, { locale: savedLanguage })
dayjs.locale(savedLanguage == 'zh_tw' ? 'zh-tw' : savedLanguage)
}
return (
<>
<Label>{t('language')}</Label>
<ButtonGroup
activeValue={savedLanguage}
onChange={(l) => setSavedLanguage(l)}
values={LANGS.map((val) => val.locale)}
names={LANGS.map((val) => t(val.name))}
/>
<Button onClick={() => handleLangChange()} className="mt-6 w-full">
<div className={`flex items-center justify-center`}>{t('save')}</div>
</Button>
</>
)
}

View File

@ -99,7 +99,7 @@ export const Row = ({ children }: RowProps) => {
)
}
export const TableDateDisplay = ({ date }: { date: string }) => (
export const TableDateDisplay = ({ date }: { date: string | number }) => (
<>
<p className="mb-0 text-th-fgd-2">{dayjs(date).format('DD MMM YYYY')}</p>
<p className="mb-0 text-xs">{dayjs(date).format('h:mma')}</p>

View File

@ -1,40 +0,0 @@
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
import { MoonIcon, SunIcon } from '@heroicons/react/outline'
import DropMenu from './DropMenu'
import { MangoIcon } from './icons'
const THEMES = [
{ name: 'Light', icon: <SunIcon className="h-4 w-4" /> },
{ name: 'Dark', icon: <MoonIcon className="h-4 w-4" /> },
{ name: 'Mango', icon: <MangoIcon className="h-4 w-4 stroke-current" /> },
]
const ThemeSwitch = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
// When mounted on client, now we can show the UI
useEffect(() => setMounted(true), [])
return (
<div id="themes-tip">
{mounted ? (
<DropMenu
button={
<div className="default-transition flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 hover:text-th-primary focus:outline-none">
{THEMES.find((t) => t.name === theme).icon}
</div>
}
value={theme}
onChange={(theme) => setTheme(theme)}
options={THEMES}
/>
) : (
<div className="h-8 w-8 rounded-full bg-th-bkg-3" />
)}
</div>
)
}
export default ThemeSwitch

View File

@ -3,22 +3,22 @@ import Link from 'next/link'
import { abbreviateAddress } from '../utils/index'
import useLocalStorageState from '../hooks/useLocalStorageState'
import MenuItem from './MenuItem'
import ThemeSwitch from './ThemeSwitch'
import useMangoStore from '../stores/useMangoStore'
import ConnectWalletButton from './ConnectWalletButton'
import { ConnectWalletButton } from 'components'
import NavDropMenu from './NavDropMenu'
import AccountsModal from './AccountsModal'
import LanguageSwitch from './LanguageSwitch'
import { DEFAULT_MARKET_KEY, initialMarket } from './SettingsModal'
import { useTranslation } from 'next-i18next'
import Settings from './Settings'
import TradeNavMenu from './TradeNavMenu'
import {
CalculatorIcon,
CurrencyDollarIcon,
LightBulbIcon,
UserAddIcon,
} from '@heroicons/react/outline'
import { MangoIcon } from './icons'
import { useWallet } from '@solana/wallet-adapter-react'
const StyledNewLabel = ({ children, ...props }) => (
<div style={{ fontSize: '0.5rem', marginLeft: '1px' }} {...props}>
@ -28,8 +28,8 @@ const StyledNewLabel = ({ children, ...props }) => (
const TopBar = () => {
const { t } = useTranslation('common')
const { publicKey } = useWallet()
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const wallet = useMangoStore((s) => s.wallet.current)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [defaultMarket] = useLocalStorageState(
DEFAULT_MARKET_KEY,
@ -83,7 +83,7 @@ const TopBar = () => {
t('referrals'),
'/referral',
false,
<UserAddIcon className="h-4 w-4" key="calculator" />,
<UserAddIcon className="h-4 w-4" key="referrals" />,
],
[
t('calculator'),
@ -91,6 +91,12 @@ const TopBar = () => {
false,
<CalculatorIcon className="h-4 w-4" key="calculator" />,
],
[
t('fees'),
'/fees',
false,
<CurrencyDollarIcon className="h-4 w-4" key="fees" />,
],
[
t('learn'),
'https://docs.mango.markets/',
@ -119,48 +125,35 @@ const TopBar = () => {
/>
</div>
</div>
<div className="flex items-center">
<div className={`pl-2`}>
<LanguageSwitch />
</div>
<div className={`pl-2`}>
<ThemeSwitch />
</div>
<div className="flex items-center space-x-2.5">
<div className="pl-2">
<Settings />
</div>
{mangoAccount &&
mangoAccount.owner.toBase58() ===
wallet?.publicKey?.toBase58() ? (
<div className="pl-2">
<button
className="rounded border border-th-bkg-4 py-1 px-2 text-xs hover:border-th-fgd-4 focus:outline-none"
onClick={() => setShowAccountsModal(true)}
>
<div className="text-xs font-normal text-th-primary">
{t('account')}
</div>
{mangoAccount.name
? mangoAccount.name
: abbreviateAddress(mangoAccount.publicKey)}
</button>
</div>
mangoAccount.owner.toBase58() === publicKey?.toBase58() ? (
<button
className="rounded border border-th-bkg-4 py-1 px-2 text-xs hover:border-th-fgd-4 focus:outline-none"
onClick={() => setShowAccountsModal(true)}
>
<div className="text-xs font-normal text-th-primary">
{t('account')}
</div>
{mangoAccount.name
? mangoAccount.name
: abbreviateAddress(mangoAccount.publicKey)}
</button>
) : null}
<div className="flex">
<div className="pl-4">
<ConnectWalletButton />
</div>
</div>
<ConnectWalletButton />
</div>
</div>
</div>
</nav>
{showAccountsModal ? (
{showAccountsModal && (
<AccountsModal
onClose={handleCloseAccounts}
isOpen={showAccountsModal}
/>
) : null}
)}
</>
)
}

View File

@ -0,0 +1,369 @@
import {
Fragment,
FunctionComponent,
useEffect,
useMemo,
useState,
} from 'react'
import { RefreshIcon } from '@heroicons/react/outline'
import { ChevronDownIcon } from '@heroicons/react/solid'
import Input, { Label } from './Input'
import Button, { LinkButton } from './Button'
import Modal from './Modal'
import { ElementTitle } from './styles'
import { useTranslation } from 'next-i18next'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import { Popover, Transition } from '@headlessui/react'
import Checkbox from './Checkbox'
import dayjs from 'dayjs'
import DatePicker from './DatePicker'
import 'react-datepicker/dist/react-datepicker.css'
interface TradeHistoryFilterModalProps {
filters: any
setFilters: any
isOpen: boolean
onClose: () => void
}
const TradeHistoryFilterModal: FunctionComponent<
TradeHistoryFilterModalProps
> = ({ filters, setFilters, isOpen, onClose }) => {
const { t } = useTranslation('common')
const [newFilters, setNewFilters] = useState({ ...filters })
const [dateFrom, setDateFrom] = useState(null)
const [dateTo, setDateTo] = useState(null)
const [sizeFrom, setSizeFrom] = useState(filters?.size?.values?.from || '')
const [sizeTo, setSizeTo] = useState(filters?.size?.values?.to || '')
const [valueFrom, setValueFrom] = useState(filters?.value?.values?.from || '')
const [valueTo, setValueTo] = useState(filters?.value?.values?.to || '')
const groupConfig = useMangoGroupConfig()
const markets = useMemo(
() =>
[...groupConfig.perpMarkets, ...groupConfig.spotMarkets].sort((a, b) =>
a.name.localeCompare(b.name)
),
[groupConfig]
)
useEffect(() => {
if (filters?.loadTimestamp?.values?.from) {
setDateFrom(new Date(filters?.loadTimestamp?.values?.from))
}
if (filters?.loadTimestamp?.values?.to) {
setDateTo(new Date(filters?.loadTimestamp?.values?.to))
}
}, [])
const handleUpdateFilterButtons = (key: string, value: any) => {
const updatedFilters = { ...newFilters }
if (Object.prototype.hasOwnProperty.call(updatedFilters, key)) {
updatedFilters[key].includes(value)
? (updatedFilters[key] = updatedFilters[key].filter((v) => v !== value))
: updatedFilters[key].push(value)
} else {
updatedFilters[key] = [value]
}
setNewFilters(updatedFilters)
}
const toggleOption = ({ id }) => {
setNewFilters((prevSelected) => {
const newSelections = prevSelected.marketName
? [...prevSelected.marketName]
: []
if (newSelections.includes(id)) {
return {
...prevSelected,
marketName: newSelections.filter((item) => item != id),
}
} else {
newSelections.push(id)
return { ...prevSelected, marketName: newSelections }
}
})
}
useEffect(() => {
if (sizeFrom && sizeTo) {
// filter should still work if users get from/to backwards
const from = sizeFrom < sizeTo ? sizeFrom : sizeTo
const to = sizeTo > sizeFrom ? sizeTo : sizeFrom
setNewFilters((prevSelected) => {
return {
...prevSelected,
size: {
condition: (size) =>
parseFloat(size) >= from && parseFloat(size) <= to,
values: { from: from, to: to },
},
}
})
}
}, [sizeFrom, sizeTo])
useEffect(() => {
if (valueFrom && valueTo) {
// filter should still work if users get from/to backwards
const from = valueFrom < valueTo ? valueFrom : valueTo
const to = valueTo > valueFrom ? valueTo : valueFrom
setNewFilters((prevSelected) => {
return {
...prevSelected,
value: {
condition: (value) => value >= from && value <= to,
values: { from: from, to: to },
},
}
})
}
}, [valueFrom, valueTo])
useEffect(() => {
if (dateFrom && dateTo) {
const dateFromTimestamp = dayjs(dateFrom).unix() * 1000
const dateToTimestamp = dayjs(dateTo).unix() * 1000
// filter should still work if users get from/to backwards
const from =
dateFromTimestamp < dateToTimestamp
? dateFromTimestamp
: dateToTimestamp
const to =
dateToTimestamp > dateFromTimestamp
? dateToTimestamp
: dateFromTimestamp
setNewFilters((prevSelected) => {
return {
...prevSelected,
loadTimestamp: {
condition: (date) => {
const timestamp = dayjs(date).unix() * 1000
return timestamp >= from && timestamp <= to
},
values: { from: from, to: to },
},
}
})
}
}, [dateFrom, dateTo])
const handleResetFilters = () => {
setFilters({})
setNewFilters({})
setDateFrom('')
setDateTo('')
setSizeFrom('')
setSizeTo('')
setValueFrom('')
setValueTo('')
}
const updateFilters = (filters) => {
setFilters(filters)
onClose()
}
return (
<Modal onClose={onClose} isOpen={isOpen}>
<Modal.Header>
<div className="flex w-full items-start justify-between pt-2">
<ElementTitle noMarginBottom>
{t('filter-trade-history')}
</ElementTitle>
<LinkButton
className="flex items-center text-th-primary"
onClick={() => handleResetFilters()}
>
<RefreshIcon className="mr-1.5 h-4 w-4" />
{t('reset')}
</LinkButton>
</div>
</Modal.Header>
<div className="pb-4">
<p className="font-bold text-th-fgd-1">{t('date')}</p>
<div className="flex items-center space-x-2">
<div className="w-1/2">
<Label>{t('from')}</Label>
<DatePicker date={dateFrom} setDate={setDateFrom} />
</div>
<div className="w-1/2">
<Label>{t('to')}</Label>
<DatePicker date={dateTo} setDate={setDateTo} />
</div>
</div>
</div>
<div className="pb-4">
<Label>{t('markets')}</Label>
<MultiSelectDropdown
options={markets}
selected={newFilters.marketName || []}
toggleOption={toggleOption}
/>
</div>
<div className="mb-4 flex items-center justify-between border-y border-th-bkg-4 py-3">
<p className="mb-0 font-bold text-th-fgd-1">{t('side')}</p>
<div className="flex space-x-2">
<FilterButton
filters={newFilters}
filterKey="side"
onClick={() => handleUpdateFilterButtons('side', 'buy')}
value="buy"
/>
<FilterButton
filters={newFilters}
filterKey="side"
onClick={() => handleUpdateFilterButtons('side', 'sell')}
value="sell"
/>
</div>
</div>
<div className="pb-4">
<p className="font-bold text-th-fgd-1">{t('size')}</p>
<div className="flex items-center space-x-2">
<div className="w-1/2">
<Label>{t('from')}</Label>
<Input
type="number"
min="0"
placeholder="0.00"
value={sizeFrom || ''}
onChange={(e) => setSizeFrom(e.target.value)}
/>
</div>
<div className="w-1/2">
<Label>{t('to')}</Label>
<Input
type="number"
min="0"
placeholder="0.00"
value={sizeTo || ''}
onChange={(e) => setSizeTo(e.target.value)}
/>
</div>
</div>
</div>
<div className="pb-4">
<p className="font-bold text-th-fgd-1">{t('value')}</p>
<div className="flex items-center space-x-2">
<div className="w-1/2">
<Label>{t('from')}</Label>
<Input
type="number"
min="0"
placeholder="0.00"
value={valueFrom || ''}
onChange={(e) => setValueFrom(e.target.value)}
/>
</div>
<div className="w-1/2">
<Label>{t('to')}</Label>
<Input
type="number"
min="0"
placeholder="0.00"
value={valueTo || ''}
onChange={(e) => setValueTo(e.target.value)}
/>
</div>
</div>
</div>
<div className="mb-6 flex items-center justify-between border-y border-th-bkg-4 py-3">
<p className="mb-0 font-bold text-th-fgd-1">{t('liquidity')}</p>
<div className="flex space-x-2">
<FilterButton
filters={newFilters}
filterKey="liquidity"
onClick={() => handleUpdateFilterButtons('liquidity', 'Maker')}
value="Maker"
/>
<FilterButton
filters={newFilters}
filterKey="liquidity"
onClick={() => handleUpdateFilterButtons('liquidity', 'Taker')}
value="Taker"
/>
</div>
</div>
<Button className="w-full" onClick={() => updateFilters(newFilters)}>
{Object.keys(filters).length > 0 ? t('update-filters') : t('filter')}
</Button>
</Modal>
)
}
const FilterButton = ({ filters, filterKey, value, onClick }) => {
const { t } = useTranslation('common')
return (
<button
className={`default-transitions rounded-full border border-th-fgd-3 px-3 py-1 text-xs text-th-fgd-1 ${
filters[filterKey]?.includes(value) &&
'border-th-primary bg-th-primary text-th-bkg-1 hover:text-th-bkg-1'
} hover:border-th-primary hover:text-th-primary`}
onClick={onClick}
>
{t(value.toLowerCase())}
</button>
)
}
const MultiSelectDropdown = ({ options, selected, toggleOption }) => {
const { t } = useTranslation('common')
return (
<Popover className="relative">
{({ open }) => (
<div className="flex flex-col">
<Popover.Button
className={`default-transition rounded-md border bg-th-bkg-1 p-3 text-th-fgd-1 hover:border-th-fgd-4 ${
open ? 'border-th-fgd-4' : 'border-th-bkg-4'
}`}
>
<div className={`flex items-center justify-between`}>
<span>
{t('filters-selected', { selectedFilters: selected.length })}
</span>
<ChevronDownIcon
className={`default-transition ml-0.5 h-5 w-5 ${
open ? 'rotate-180 transform' : 'rotate-360 transform'
}`}
aria-hidden="true"
/>
</div>
</Popover.Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition-all ease-in duration-200"
enterFrom="opacity-0 transform scale-75"
enterTo="opacity-100 transform scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Panel className="absolute top-12 z-10 h-72 w-full overflow-y-auto">
<div className="relative space-y-2.5 rounded-md bg-th-bkg-3 p-3">
{options.map((option) => {
const isSelected = selected.includes(option.name)
return (
<Checkbox
checked={isSelected}
className="mr-2"
key={option.name}
onChange={() => toggleOption({ id: option.name })}
>
{option.name}
</Checkbox>
)
})}
</div>
</Popover.Panel>
</Transition>
</div>
)}
</Popover>
)
}
export default TradeHistoryFilterModal

View File

@ -3,7 +3,7 @@ import BN from 'bn.js'
import Link from 'next/link'
import { useRouter } from 'next/router'
import SideBadge from './SideBadge'
import { LinkButton } from './Button'
import Button, { LinkButton } from './Button'
import { useSortableData } from '../hooks/useSortableData'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
@ -20,29 +20,41 @@ import { formatUsdValue } from '../utils'
import { useTranslation } from 'next-i18next'
import Pagination from './Pagination'
import usePagination from '../hooks/usePagination'
import { useEffect } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useFilteredData } from '../hooks/useFilteredData'
import TradeHistoryFilterModal from './TradeHistoryFilterModal'
import {
FilterIcon,
InformationCircleIcon,
RefreshIcon,
SaveIcon,
} from '@heroicons/react/outline'
import { fetchHourlyPerformanceStats } from './account_page/AccountOverview'
import useMangoStore from '../stores/useMangoStore'
import Loading from './Loading'
import { exportDataToCSV } from '../utils/export'
import Tooltip from './Tooltip'
import { useWallet } from '@solana/wallet-adapter-react'
const renderTradeDateTime = (timestamp: BN | string) => {
let date
const formatTradeDateTime = (timestamp: BN | string) => {
// don't compare to BN because of npm maddness
// prototypes can be different due to multiple versions being imported
if (typeof timestamp === 'string') {
date = new Date(timestamp)
return timestamp
} else {
date = new Date(timestamp.toNumber() * 1000)
return timestamp.toNumber() * 1000
}
return (
<>
<div>{date.toLocaleDateString()}</div>
<div className="text-xs text-th-fgd-3">{date.toLocaleTimeString()}</div>
</>
)
}
const TradeHistoryTable = ({ numTrades }: { numTrades?: number }) => {
const TradeHistoryTable = ({
numTrades,
showExportPnl,
}: {
numTrades?: number
showExportPnl?: boolean
}) => {
const { t } = useTranslation('common')
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const { asPath } = useRouter()
const { width } = useViewport()
const tradeHistoryAndLiquidations = useMangoStore(
@ -52,6 +64,13 @@ const TradeHistoryTable = ({ numTrades }: { numTrades?: number }) => {
(t) => !('liqor' in t)
)
const isMobile = width ? width < breakpoints.md : false
const [filters, setFilters] = useState({})
const [showFiltersModal, setShowFiltersModal] = useState(false)
const [loadExportData, setLoadExportData] = useState(false)
const filteredData = useFilteredData(tradeHistory, filters)
const initialLoad = useMangoStore((s) => s.tradeHistory.initialLoad)
const { publicKey } = useWallet()
const {
paginatedData,
@ -63,14 +82,14 @@ const TradeHistoryTable = ({ numTrades }: { numTrades?: number }) => {
lastPage,
setData,
data,
} = usePagination(tradeHistory || [], { perPage: 100 })
} = usePagination(filteredData, { perPage: 100 })
const { items, requestSort, sortConfig } = useSortableData(paginatedData)
useEffect(() => {
if (tradeHistory?.length && data?.length !== tradeHistory?.length) {
setData(tradeHistory)
if (data?.length !== filteredData?.length) {
setData(filteredData)
}
}, [tradeHistory])
}, [filteredData])
const renderMarketName = (trade: any) => {
if (
@ -94,329 +113,477 @@ const TradeHistoryTable = ({ numTrades }: { numTrades?: number }) => {
}
}
const exportPerformanceDataToCSV = async () => {
setLoadExportData(true)
const exportData = await fetchHourlyPerformanceStats(
mangoAccount.publicKey.toString(),
10000
)
const dataToExport = exportData.map((row) => {
const timestamp = new Date(row.time)
return {
timestamp: `${timestamp.toLocaleDateString()} ${timestamp.toLocaleTimeString()}`,
account_equity: row.account_equity,
pnl: row.pnl,
}
})
const title = `${
mangoAccount.name || mangoAccount.publicKey
}-Performance-${new Date().toLocaleDateString()}`
const headers = ['Timestamp', 'Account Equity', 'PNL']
exportDataToCSV(dataToExport, title, headers, t)
setLoadExportData(false)
}
const hasActiveFilter = useMemo(() => {
return tradeHistory.length !== filteredData.length
}, [data, filteredData])
const mangoAccountPk = useMemo(() => {
console.log('new mango account')
return mangoAccount.publicKey.toString()
}, [mangoAccount])
const canWithdraw =
mangoAccount && publicKey ? mangoAccount.owner.equals(publicKey) : false
return (
<div className={`flex flex-col sm:pb-4`}>
<div className={`overflow-x-auto sm:-mx-6 lg:-mx-8`}>
<div className={`inline-block min-w-full align-middle sm:px-6 lg:px-8`}>
{tradeHistory && tradeHistory.length ? (
!isMobile ? (
<>
<Table>
<thead>
<TrHead>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('marketName')}
>
<span className="font-normal">{t('market')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'marketName'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('side')}
>
<span className="font-normal">{t('side')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'side'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('size')}
>
<span className="font-normal">{t('size')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'size'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('price')}
>
<span className="font-normal">{t('price')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'price'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('value')}
>
<span className="font-normal">{t('value')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'value'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('liquidity')}
>
<span className="font-normal">{t('liquidity')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'liquidity'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('feeCost')}
>
<span className="font-normal">{t('fee')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'feeCost'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('loadTimestamp')}
>
<span className="font-normal">{t('date')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'loadTimestamp'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
</TrHead>
</thead>
<tbody>
{items.map((trade: any) => {
return (
<TrBody key={`${trade.seqNum}${trade.marketName}`}>
<Td className="!py-2 ">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${trade.marketName
.split(/-|\//)[0]
.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{renderMarketName(trade)}
</div>
</Td>
<Td className="!py-2 ">
<SideBadge side={trade.side} />
</Td>
<Td className="!py-2 ">{trade.size}</Td>
<Td className="!py-2 ">
$
{new Intl.NumberFormat('en-US').format(trade.price)}
</Td>
<Td className="!py-2 ">
{formatUsdValue(trade.value)}
</Td>
<Td className="!py-2 ">
{t(trade.liquidity.toLowerCase())}
</Td>
<Td className="!py-2 ">
{formatUsdValue(trade.feeCost)}
</Td>
<Td className="!py-2">
<>
<div className="flex flex-col pb-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center">
<h4 className="mb-0 flex items-center text-th-fgd-1">
{!initialLoad ? <Loading className="mr-2" /> : data.length}{' '}
{data.length === 1 ? 'Trade' : 'Trades'}
</h4>
<Tooltip
content={
<div className="mr-4 text-xs text-th-fgd-3">
{t('delay-displaying-recent')} {t('use-explorer-one')}
<a
href={`https://explorer.solana.com/address/${mangoAccount.publicKey.toString()}`}
target="_blank"
rel="noopener noreferrer"
>
{t('use-explorer-two')}
</a>
{t('use-explorer-three')}
</div>
}
>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
</Tooltip>
</div>
<div className="flex flex-col space-y-3 pl-2 sm:flex-row sm:items-center sm:space-y-0 sm:space-x-3">
{hasActiveFilter ? (
<LinkButton
className="order-4 mt-3 flex items-center justify-end whitespace-nowrap text-xs sm:order-first sm:mt-0"
onClick={() => setFilters({})}
>
<RefreshIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
{t('reset-filters')}
</LinkButton>
) : null}
{tradeHistory.length >= 15 &&
tradeHistory.length <= 10000 &&
initialLoad ? (
<Button
className="order-3 mt-3 flex h-8 items-center justify-center whitespace-nowrap pt-0 pb-0 pl-3 pr-3 text-xs sm:order-first sm:mt-0"
onClick={() => setShowFiltersModal(true)}
>
<FilterIcon className="mr-1.5 h-4 w-4" />
{t('filter')}
</Button>
) : null}
{canWithdraw && showExportPnl ? (
<Button
className={`flex h-8 items-center justify-center whitespace-nowrap pt-0 pb-0 pl-3 pr-3 text-xs`}
onClick={exportPerformanceDataToCSV}
>
{loadExportData ? (
<Loading />
) : (
<div className={`flex items-center`}>
<SaveIcon className={`mr-1.5 h-4 w-4`} />
{t('export-pnl-csv')}
</div>
)}
</Button>
) : null}
{canWithdraw ? (
<div className={`flex items-center`}>
<a
className={`default-transition flex h-8 w-full items-center justify-center whitespace-nowrap rounded-full bg-th-bkg-button pt-0 pb-0 pl-3 pr-3 text-xs font-bold text-th-fgd-1 hover:text-th-fgd-1 hover:brightness-[1.1]`}
href={`https://event-history-api.herokuapp.com/all_trades_csv?mango_account=${mangoAccountPk}&open_orders=${mangoAccount.spotOpenOrders
.filter(
(e) => e.toString() !== '11111111111111111111111111111111'
)
.join(',')}`}
download
target="_blank"
rel="noopener noreferrer"
>
<SaveIcon className={`mr-1.5 h-4 w-4`} />
{t('export-trades-csv')}
<Tooltip content={t('trade-export-disclaimer')}>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-3" />
</Tooltip>
</a>
</div>
) : null}
</div>
</div>
<div className={`flex flex-col sm:pb-4`}>
<div className={`overflow-x-auto sm:-mx-6 lg:-mx-8`}>
<div
className={`inline-block min-w-full align-middle sm:px-6 lg:px-8`}
>
{tradeHistory && paginatedData.length > 0 ? (
!isMobile ? (
<>
<Table>
<thead>
<TrHead>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('marketName')}
>
<span className="font-normal">{t('market')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'marketName'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('side')}
>
<span className="font-normal">{t('side')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'side'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('size')}
>
<span className="font-normal">{t('size')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'size'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('price')}
>
<span className="font-normal">{t('price')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'price'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('value')}
>
<span className="font-normal">{t('value')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'value'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('liquidity')}
>
<span className="font-normal">
{t('liquidity')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'liquidity'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('feeCost')}
>
<span className="font-normal">{t('fee')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'feeCost'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('loadTimestamp')}
>
<span className="font-normal">
{t('approximate-time')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'loadTimestamp'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
</TrHead>
</thead>
<tbody>
{items.map((trade: any) => {
return (
<TrBody key={`${trade.seqNum}${trade.marketName}`}>
<Td className="!py-2 ">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${trade.marketName
.split(/-|\//)[0]
.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{renderMarketName(trade)}
</div>
</Td>
<Td className="!py-2 ">
<SideBadge side={trade.side} />
</Td>
<Td className="!py-2 ">{trade.size}</Td>
<Td className="!py-2 ">
$
{new Intl.NumberFormat('en-US').format(
trade.price
)}
</Td>
<Td className="!py-2 ">
{formatUsdValue(trade.value)}
</Td>
<Td className="!py-2 ">
{t(trade.liquidity.toLowerCase())}
</Td>
<Td className="!py-2 ">
{formatUsdValue(trade.feeCost)}
</Td>
<Td className="!py-2">
{trade.loadTimestamp || trade.timestamp ? (
<TableDateDisplay
date={formatTradeDateTime(
trade.loadTimestamp || trade.timestamp
)}
/>
) : (
t('recent')
)}
</Td>
<Td className="keep-break w-[0.1%] !py-2">
{trade.marketName.includes('PERP') ? (
<a
className="text-xs text-th-fgd-4 underline underline-offset-4"
target="_blank"
rel="noopener noreferrer"
href={`/account?pubkey=${
trade.liquidity === 'Taker'
? trade.maker
: trade.taker
}`}
>
{t('view-counterparty')}
</a>
) : null}
</Td>
</TrBody>
)
})}
</tbody>
</Table>
{numTrades && items.length > numTrades ? (
<div className="mt-4 flex items-center justify-center">
<Link href="/account" shallow={true}>
{t('view-all-trades')}
</Link>
</div>
) : (
<div className="flex items-center justify-end">
<Pagination
page={page}
totalPages={totalPages}
nextPage={nextPage}
lastPage={lastPage}
firstPage={firstPage}
previousPage={previousPage}
/>
</div>
)}
</>
) : (
paginatedData.map((trade: any, index) => (
<ExpandableRow
buttonTemplate={
<>
<div className="text-fgd-1 flex w-full items-center justify-between">
<div className="text-left">
{trade.loadTimestamp || trade.timestamp ? (
<TableDateDisplay
date={trade.loadTimestamp || trade.timestamp}
date={formatTradeDateTime(
trade.loadTimestamp || trade.timestamp
)}
/>
) : (
t('recent')
)}
</Td>
<Td className="keep-break w-[0.1%] !py-2">
{trade.marketName.includes('PERP') ? (
<a
className="text-xs text-th-fgd-4 underline underline-offset-4"
target="_blank"
rel="noopener noreferrer"
href={`/account?pubkey=${
trade.liquidity === 'Taker'
? trade.maker
: trade.taker
}`}
>
{t('view-counterparty')}
</a>
) : null}
</Td>
</TrBody>
)
})}
</tbody>
</Table>
{numTrades && items.length > numTrades ? (
<div className="mt-4 flex items-center justify-center">
<Link href="/account" shallow={true}>
{t('view-all-trades')}
</Link>
</div>
) : (
<div className="flex items-center justify-end">
<Pagination
page={page}
totalPages={totalPages}
nextPage={nextPage}
lastPage={lastPage}
firstPage={firstPage}
previousPage={previousPage}
/>
</div>
)}
</>
) : (
paginatedData.map((trade: any, index) => (
<ExpandableRow
buttonTemplate={
<>
<div className="text-fgd-1 flex w-full items-center justify-between">
<div className="text-left">
{trade.loadTimestamp || trade.timestamp
? renderTradeDateTime(
trade.loadTimestamp || trade.timestamp
)
: t('recent')}
</div>
<div>
<div className="text-right">
<div className="mb-0.5 flex items-center text-left">
<img
alt=""
width="16"
height="16"
src={`/assets/icons/${trade.marketName
.split(/-|\//)[0]
.toLowerCase()}.svg`}
className={`mr-1.5`}
/>
{trade.marketName}
</div>
<div className="text-xs text-th-fgd-3">
<span
className={`mr-1
</div>
<div>
<div className="text-right">
<div className="mb-0.5 flex items-center text-left">
<img
alt=""
width="16"
height="16"
src={`/assets/icons/${trade.marketName
.split(/-|\//)[0]
.toLowerCase()}.svg`}
className={`mr-1.5`}
/>
{trade.marketName}
</div>
<div className="text-xs text-th-fgd-3">
<span
className={`mr-1
${
trade.side === 'buy' || trade.side === 'long'
? 'text-th-green'
: 'text-th-red'
}
`}
>
{trade.side.toUpperCase()}
</span>
{trade.size}
>
{trade.side.toUpperCase()}
</span>
{trade.size}
</div>
</div>
</div>
</div>
</div>
</>
}
key={`${index}`}
panelTemplate={
<div className="grid grid-flow-row grid-cols-2 gap-4">
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('price')}
</>
}
key={`${index}`}
panelTemplate={
<div className="grid grid-flow-row grid-cols-2 gap-4">
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('price')}
</div>
{formatUsdValue(trade.price)}
</div>
{formatUsdValue(trade.price)}
</div>
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('value')}
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('value')}
</div>
{formatUsdValue(trade.value)}
</div>
{formatUsdValue(trade.value)}
</div>
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('liquidity')}
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('liquidity')}
</div>
{trade.liquidity}
</div>
{trade.liquidity}
</div>
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('fee')}
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('fee')}
</div>
{formatUsdValue(trade.feeCost)}
</div>
{formatUsdValue(trade.feeCost)}
</div>
</div>
}
/>
))
)
) : (
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
{t('no-history')}
{asPath === '/account' ? (
<Link href={'/'} shallow={true}>
<a className="ml-2 inline-flex py-0">{t('make-trade')}</a>
</Link>
) : null}
</div>
)}
}
/>
))
)
) : hasActiveFilter ? (
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
{t('no-trades-found')}
</div>
) : (
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
{t('no-history')}
{asPath === '/account' ? (
<Link href={'/'} shallow={true}>
<a className="ml-2 inline-flex py-0">{t('make-trade')}</a>
</Link>
) : null}
</div>
)}
</div>
</div>
</div>
</div>
{showFiltersModal ? (
<TradeHistoryFilterModal
filters={filters}
setFilters={setFilters}
isOpen={showFiltersModal}
onClose={() => setShowFiltersModal(false)}
/>
) : null}
</>
)
}

View File

@ -240,14 +240,14 @@ export const FavoriteMarketButton = ({ market }) => {
return favoriteMarkets.find((mkt) => mkt.name === market.name) ? (
<button
className="default-transition text-th-primary hover:text-th-fgd-3"
className="default-transition flex items-center justify-center text-th-primary hover:text-th-fgd-3"
onClick={() => removeFromFavorites(market)}
>
<FilledStarIcon className="h-5 w-5" />
</button>
) : (
<button
className="default-transition text-th-fgd-4 hover:text-th-primary"
className="default-transition flex items-center justify-center text-th-fgd-4 hover:text-th-primary"
onClick={() => addToFavorites(market)}
>
<StarIcon className="h-5 w-5" />

View File

@ -17,6 +17,7 @@ import { sleep, formatUsdValue, usdFormatter, roundPerpSize } from '../../utils'
import { PerpTriggerOrder } from '../../@types/types'
import { useTranslation } from 'next-i18next'
import useLocalStorageState from '../../hooks/useLocalStorageState'
import { useWallet, Wallet } from '@solana/wallet-adapter-react'
export interface ChartContainerProps {
symbol: ChartingLibraryWidgetOptions['symbol']
@ -40,6 +41,7 @@ const TVChartContainer = () => {
const { t } = useTranslation(['common', 'tv-chart'])
const { theme } = useTheme()
const { width } = useViewport()
const { wallet } = useWallet()
const [chartReady, setChartReady] = useState(false)
const [showOrderLinesLocalStorage, toggleShowOrderLinesLocalStorage] =
useLocalStorageState(SHOW_ORDER_LINES_KEY, true)
@ -51,7 +53,6 @@ const TVChartContainer = () => {
const selectedMarketConfig = useMangoStore((s) => s.selectedMarket.config)
const openOrders = useMangoStore((s) => s.selectedMangoAccount.openOrders)
const actions = useMangoStore((s) => s.actions)
const selectedMarketPrice = useMangoStore((s) => s.selectedMarket.markPrice)
const isMobile = width ? width < breakpoints.sm : false
const mangoClient = useMangoStore.getState().connection.client
@ -216,9 +217,9 @@ const TVChartContainer = () => {
const handleCancelOrder = async (
order: Order | PerpOrder | PerpTriggerOrder,
market: Market | PerpMarket
market: Market | PerpMarket,
wallet: Wallet
) => {
const wallet = useMangoStore.getState().wallet.current
const selectedMangoGroup =
useMangoStore.getState().selectedMangoGroup.current
const selectedMangoAccount =
@ -231,7 +232,7 @@ const TVChartContainer = () => {
txid = await mangoClient.cancelSpotOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet,
wallet?.adapter,
// @ts-ignore
market,
order as Order
@ -241,14 +242,14 @@ const TVChartContainer = () => {
txid = await mangoClient.removeAdvancedOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet,
wallet?.adapter,
(order as PerpTriggerOrder).orderId
)
} else {
txid = await mangoClient.cancelPerpOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet,
wallet?.adapter,
market,
order as PerpOrder,
false
@ -273,7 +274,8 @@ const TVChartContainer = () => {
const handleModifyOrder = async (
order: Order | PerpOrder,
market: Market | PerpMarket,
price: number
price: number,
wallet: Wallet
) => {
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
@ -282,7 +284,6 @@ const TVChartContainer = () => {
useMangoStore.getState().accountInfos[marketConfig.asksKey.toString()]
const bidInfo =
useMangoStore.getState().accountInfos[marketConfig.bidsKey.toString()]
const wallet = useMangoStore.getState().wallet.current
const referrerPk = useMangoStore.getState().referrerPk
if (!wallet || !mangoGroup || !mangoAccount || !market) return
@ -306,7 +307,7 @@ const TVChartContainer = () => {
mangoGroup.mangoCache,
// @ts-ignore
market,
wallet,
wallet?.adapter,
order as Order,
order.side,
orderPrice,
@ -319,7 +320,7 @@ const TVChartContainer = () => {
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
wallet?.adapter,
order as PerpOrder,
order.side,
orderPrice,
@ -351,12 +352,15 @@ const TVChartContainer = () => {
function drawLine(order, market) {
const orderSizeUi = roundPerpSize(order.size, market.config.baseSymbol)
if (!tvWidgetRef.current.chart()) return
return tvWidgetRef.current
.chart()
.createOrderLine({ disableUndo: false })
.onMove(function () {
const currentOrderPrice = order.price
const updatedOrderPrice = this.getPrice()
const selectedMarketPrice =
useMangoStore.getState().selectedMarket.markPrice
if (!order.perpTrigger?.clientOrderId) {
if (
(order.side === 'buy' &&
@ -390,7 +394,12 @@ const TVChartContainer = () => {
}),
callback: (res) => {
if (res) {
handleModifyOrder(order, market.account, updatedOrderPrice)
handleModifyOrder(
order,
market.account,
updatedOrderPrice,
wallet
)
} else {
this.setPrice(currentOrderPrice)
}
@ -418,7 +427,7 @@ const TVChartContainer = () => {
}),
callback: (res) => {
if (res) {
handleCancelOrder(order, market.account)
handleCancelOrder(order, market.account, wallet)
}
},
})

View File

@ -6,16 +6,9 @@ import PositionsTable from './PerpPositionsTable'
import TradeHistoryTable from './TradeHistoryTable'
import ManualRefresh from './ManualRefresh'
import Tabs from './Tabs'
import FeeDiscountsTable from './FeeDiscountsTable'
import { marketConfigSelector } from '../stores/selectors'
const TABS = [
'Balances',
'Orders',
'Positions',
'Trade History',
'Fee Discount',
]
const TABS = ['Balances', 'Orders', 'Positions', 'Trade History']
const UserInfoTabs = ({ activeTab, setActiveTab }) => {
const totalOpenOrders = useMangoStore(
@ -60,8 +53,6 @@ const TabContent = ({ activeTab }) => {
return <TradeHistoryTable numTrades={100} />
case 'Positions':
return <PositionsTable />
case 'Fee Discount':
return <FeeDiscountsTable />
default:
return <BalancesTable clickToPopulateTradeForm />
}

View File

@ -0,0 +1,39 @@
import React, { useEffect } from 'react'
import useMangoStore from 'stores/useMangoStore'
import { useWallet } from '@solana/wallet-adapter-react'
/*
* This component listens for when the Solana Wallet Adapter connects to a wallet.
* When a wallet is connected we stitch the Solana Wallet Adapter wallet to our Mango Store's wallet.
* Eventually we can remove this listener when we move to only use one Wallet, preferably the Wallet Adapter Wallet.
*/
export const WalletListener: React.FC = () => {
const set = useMangoStore((s) => s.set)
const actions = useMangoStore((s) => s.actions)
const { wallet } = useWallet()
const connecting = wallet?.adapter?.connecting
useEffect(() => {
const onConnect = async () => {
set((state) => {
state.selectedMangoAccount.initialLoad = true
})
await actions.fetchAllMangoAccounts(wallet)
actions.fetchProfilePicture(wallet)
actions.reloadOrders()
actions.fetchTradeHistory()
actions.fetchWalletTokens()
}
if (connecting) {
onConnect()
}
}, [connecting, set, actions, wallet])
return null
}

View File

@ -0,0 +1,330 @@
import {
Adapter,
SendTransactionOptions,
WalletError,
WalletNotConnectedError,
WalletNotReadyError,
WalletReadyState,
} from '@solana/wallet-adapter-base'
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
import { useLocalStorageStringState } from 'hooks/useLocalStorageState'
import React, {
FC,
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { Wallet, WalletContext } from '@solana/wallet-adapter-react'
export interface WalletProviderProps {
children: ReactNode
wallets: Adapter[]
autoConnect?: boolean
onError?: (error: WalletError) => void
localStorageKey?: string
}
class WalletNotSelectedError extends WalletError {
name = 'WalletNotSelectedError'
}
const initialState: {
wallet: Wallet | null
adapter: Adapter | null
publicKey: PublicKey | null
connected: boolean
} = {
wallet: null,
adapter: null,
publicKey: null,
connected: false,
}
/*
* This is a refactored version of the Solana Labs wallet.
* For Mango's use case we needed to remove the clearing of the wallet after disconnect and errors.
* Original: https://github.com/solana-labs/wallet-adapter/blob/master/packages/core/react/src/WalletProvider.tsx
*/
export const WalletProvider: FC<WalletProviderProps> = ({
children,
wallets: adapters,
autoConnect = false,
onError,
localStorageKey = 'walletName',
}) => {
const [name, setName] = useLocalStorageStringState(localStorageKey, null)
const [{ wallet, adapter, publicKey, connected }, setState] =
useState(initialState)
const readyState = adapter?.readyState || WalletReadyState.Unsupported
const [connecting, setConnecting] = useState(false)
const [disconnecting, setDisconnecting] = useState(false)
const isConnecting = useRef(false)
const isDisconnecting = useRef(false)
const isUnloading = useRef(false)
// Wrap adapters to conform to the `Wallet` interface
const [wallets, setWallets] = useState(() =>
adapters.map((adapter) => ({
adapter,
readyState: adapter.readyState,
}))
)
// When the wallets change, start to listen for changes to their `readyState`
useEffect(() => {
function handleReadyStateChange(
this: Adapter,
readyState: WalletReadyState
) {
setWallets((prevWallets) => {
const walletIndex = prevWallets.findIndex(
({ adapter }) => adapter.name === this.name
)
if (walletIndex === -1) return prevWallets
return [
...prevWallets.slice(0, walletIndex),
{ ...prevWallets[walletIndex], readyState },
...prevWallets.slice(walletIndex + 1),
]
})
}
for (const adapter of adapters) {
adapter.on('readyStateChange', handleReadyStateChange, adapter)
}
return () => {
for (const adapter of adapters) {
adapter.off('readyStateChange', handleReadyStateChange, adapter)
}
}
}, [adapters])
// When the selected wallet changes, initialize the state
useEffect(() => {
const wallet = wallets.find(({ adapter }) => adapter.name === name)
if (wallet) {
setState({
wallet,
adapter: wallet.adapter,
connected: wallet.adapter.connected,
publicKey: wallet.adapter.publicKey,
})
} else {
setState(initialState)
}
}, [name, wallets])
// If autoConnect is enabled, try to connect when the adapter changes and is ready
useEffect(() => {
if (
isConnecting.current ||
connecting ||
connected ||
!autoConnect ||
!adapter ||
!(
readyState === WalletReadyState.Installed ||
readyState === WalletReadyState.Loadable
)
)
return
;(async function () {
isConnecting.current = true
setConnecting(true)
try {
await adapter.connect()
} catch (error: any) {
// Clear the selected wallet
// Don't throw error, but handleError will still be called
} finally {
setConnecting(false)
isConnecting.current = false
}
})()
}, [isConnecting, connecting, connected, autoConnect, adapter, readyState])
// If the window is closing or reloading, ignore disconnect and error events from the adapter
useEffect(() => {
function listener() {
isUnloading.current = true
}
window.addEventListener('beforeunload', listener)
return () => window.removeEventListener('beforeunload', listener)
}, [isUnloading])
// Handle the adapter's connect event
const handleConnect = useCallback(() => {
if (!adapter) return
setState((state) => ({
...state,
connected: adapter.connected,
publicKey: adapter.publicKey,
}))
}, [adapter])
// Handle the adapter's disconnect event
const handleDisconnect = useCallback(() => {
setState((state) => ({
...state,
connected: adapter.connected,
publicKey: null,
}))
}, [adapter])
// Handle the adapter's error event, and local errors
const handleError = useCallback(
(error: WalletError) => {
// Call onError unless the window is unloading
if (!isUnloading.current) (onError || console.error)(error)
return error
},
[isUnloading, onError]
)
// Setup and teardown event listeners when the adapter changes
useEffect(() => {
if (adapter) {
adapter.on('connect', handleConnect)
adapter.on('disconnect', handleDisconnect)
adapter.on('error', handleError)
return () => {
adapter.off('connect', handleConnect)
adapter.off('disconnect', handleDisconnect)
adapter.off('error', handleError)
}
}
}, [adapter, handleConnect, handleDisconnect, handleError])
// When the adapter changes, disconnect the old one
useEffect(() => {
return () => {
adapter?.disconnect()
}
}, [adapter])
// Connect the adapter to the wallet
const connect = useCallback(async () => {
if (isConnecting.current || connecting || disconnecting || connected) return
if (!adapter) throw handleError(new WalletNotSelectedError())
if (
!(
readyState === WalletReadyState.Installed ||
readyState === WalletReadyState.Loadable
)
) {
if (typeof window !== 'undefined') {
window.open(adapter.url, '_blank')
}
throw handleError(new WalletNotReadyError())
}
isConnecting.current = true
setConnecting(true)
await adapter.connect()
setConnecting(false)
isConnecting.current = false
}, [
isConnecting,
connecting,
disconnecting,
connected,
adapter,
readyState,
handleError,
])
// Disconnect the adapter from the wallet
const disconnect = useCallback(async () => {
if (isDisconnecting.current || disconnecting) return
if (!adapter) return
isDisconnecting.current = true
setDisconnecting(true)
try {
await adapter.disconnect()
} catch (error: any) {
setDisconnecting(false)
isDisconnecting.current = false
throw error
}
}, [isDisconnecting, disconnecting, adapter])
// Send a transaction using the provided connection
const sendTransaction = useCallback(
async (
transaction: Transaction,
connection: Connection,
options?: SendTransactionOptions
) => {
if (!adapter) throw handleError(new WalletNotSelectedError())
if (!connected) throw handleError(new WalletNotConnectedError())
return await adapter.sendTransaction(transaction, connection, options)
},
[adapter, handleError, connected]
)
// Sign a transaction if the wallet supports it
const signTransaction = useMemo(
() =>
adapter && 'signTransaction' in adapter
? async (transaction: Transaction): Promise<Transaction> => {
if (!connected) throw handleError(new WalletNotConnectedError())
return await adapter.signTransaction(transaction)
}
: undefined,
[adapter, handleError, connected]
)
// Sign multiple transactions if the wallet supports it
const signAllTransactions = useMemo(
() =>
adapter && 'signAllTransactions' in adapter
? async (transactions: Transaction[]): Promise<Transaction[]> => {
if (!connected) throw handleError(new WalletNotConnectedError())
return await adapter.signAllTransactions(transactions)
}
: undefined,
[adapter, handleError, connected]
)
// Sign an arbitrary message if the wallet supports it
const signMessage = useMemo(
() =>
adapter && 'signMessage' in adapter
? async (message: Uint8Array): Promise<Uint8Array> => {
if (!connected) throw handleError(new WalletNotConnectedError())
return await adapter.signMessage(message)
}
: undefined,
[adapter, handleError, connected]
)
return (
<WalletContext.Provider
value={{
autoConnect,
wallets,
wallet,
publicKey,
connected,
connecting,
disconnecting,
select: setName,
connect,
disconnect,
sendTransaction,
signTransaction,
signAllTransactions,
signMessage,
}}
>
{children}
</WalletContext.Provider>
)
}

View File

@ -0,0 +1,2 @@
export * from './WalletListener'
export * from './WalletProvider'

View File

@ -1,16 +1,13 @@
import { Fragment } from 'react'
import React, { Fragment } from 'react'
import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/solid'
import useMangoStore from '../stores/useMangoStore'
import { WALLET_PROVIDERS } from '../utils/wallet-adapters'
import { useWallet, Wallet } from '@solana/wallet-adapter-react'
export default function WalletSelect() {
const setMangoStore = useMangoStore((s) => s.set)
export const WalletSelect: React.FC<{ wallets: Wallet[] }> = ({ wallets }) => {
const { select } = useWallet()
const handleSelectProvider = (url) => {
setMangoStore((state) => {
state.wallet.providerUrl = url
})
if (!wallets?.length) {
return null
}
return (
@ -38,15 +35,21 @@ export default function WalletSelect() {
leaveTo="opacity-0"
>
<Menu.Items className="absolute right-0 z-20 w-44 rounded-b-md bg-th-bkg-3 px-4 py-2.5 outline-none">
{WALLET_PROVIDERS.map(({ name, url, icon }) => (
<Menu.Item key={name}>
{wallets?.map((wallet, index) => (
<Menu.Item key={index}>
<button
className="flex w-full flex-row items-center justify-between rounded-none py-1.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => handleSelectProvider(url)}
onClick={() => {
select(wallet.adapter.name)
}}
>
<div className="flex items-center">
<img src={icon} className="mr-2 h-4 w-4" />
{name}
<img
src={wallet.adapter.icon}
className="mr-2 h-4 w-4"
alt={`${wallet.adapter.name} icon`}
/>
{wallet.adapter.name}
</div>
</button>
</Menu.Item>

View File

@ -23,6 +23,7 @@ import {
import { notify } from '../utils/notifications'
import { useTranslation } from 'next-i18next'
import { ExpandableRow } from './TableElements'
import { useWallet } from '@solana/wallet-adapter-react'
interface WithdrawModalProps {
onClose: () => void
@ -50,7 +51,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
const [includeBorrow, setIncludeBorrow] = useState(borrow)
const [simulation, setSimulation] = useState(null)
const [showSimulation, setShowSimulation] = useState(false)
const { wallet } = useWallet()
const actions = useMangoStore((s) => s.actions)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
@ -161,6 +162,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
amount: Number(inputAmount),
token: mangoGroup.tokens[tokenIndex].mint,
allowBorrow: includeBorrow,
wallet,
})
.then((txid: string) => {
setSubmitting(false)

View File

@ -6,6 +6,7 @@ import Loading from './Loading'
import Modal from './Modal'
import { msrmMints } from '@blockworks-foundation/mango-client'
import { useTranslation } from 'next-i18next'
import { useWallet } from '@solana/wallet-adapter-react'
const WithdrawMsrmModal = ({ onClose, isOpen }) => {
const { t } = useTranslation('common')
@ -13,8 +14,8 @@ const WithdrawMsrmModal = ({ onClose, isOpen }) => {
const actions = useMangoStore((s) => s.actions)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const wallet = useMangoStore((s) => s.wallet.current)
const walletTokens = useMangoStore((s) => s.wallet.tokens)
const { wallet } = useWallet()
const cluster = useMangoStore.getState().connection.cluster
const handleMsrmWithdraw = async () => {
@ -27,7 +28,7 @@ const WithdrawMsrmModal = ({ onClose, isOpen }) => {
const txid = await mangoClient.withdrawMsrm(
mangoGroup,
mangoAccount,
wallet,
wallet?.adapter,
ownerMsrmAccount.account.publicKey,
1
)

View File

@ -20,8 +20,8 @@ import { Table, Td, Th, TrBody, TrHead } from '../TableElements'
import { ExpandableRow } from '../TableElements'
import MobileTableHeader from '../mobile/MobileTableHeader'
import { useTranslation } from 'next-i18next'
import { walletSelector } from '../../stores/selectors'
import Tooltip from '../Tooltip'
import { useWallet } from '@solana/wallet-adapter-react'
export default function AccountBorrows() {
const { t } = useTranslation('common')
@ -30,11 +30,10 @@ export default function AccountBorrows() {
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const mangoConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const wallet = useMangoStore(walletSelector)
const { publicKey, connected } = useWallet()
const loadingMangoAccount = useMangoStore(
(s) => s.selectedMangoAccount.initialLoad
)
const connected = useMangoStore((s) => s.wallet.connected)
const [borrowSymbol, setBorrowSymbol] = useState('')
const [depositToSettle, setDepositToSettle] = useState(null)
@ -42,7 +41,7 @@ export default function AccountBorrows() {
const [showDepositModal, setShowDepositModal] = useState(false)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const canWithdraw = mangoAccount?.owner.equals(wallet.publicKey)
const canWithdraw = publicKey && mangoAccount?.owner.equals(publicKey)
const handleCloseWithdraw = useCallback(() => {
setShowBorrowModal(false)

View File

@ -1,7 +1,11 @@
import { useState, useEffect, useMemo } from 'react'
import { useTranslation } from 'next-i18next'
import { ArrowSmDownIcon, ExternalLinkIcon } from '@heroicons/react/outline'
import { SaveIcon } from '@heroicons/react/outline'
import {
ArrowSmDownIcon,
ExternalLinkIcon,
InformationCircleIcon,
SaveIcon,
} from '@heroicons/react/outline'
import {
getMarketByBaseSymbolAndKind,
PerpMarket,
@ -20,13 +24,10 @@ import {
import { LinkButton } from '../Button'
import { useSortableData } from '../../hooks/useSortableData'
import { formatUsdValue } from '../../utils'
import Tooltip from '../Tooltip'
import { exportDataToCSV } from '../../utils/export'
import { notify } from '../../utils/notifications'
import Button from '../Button'
import Loading from '../Loading'
import { fetchHourlyPerformanceStats } from './AccountOverview'
import Tooltip from '../Tooltip'
import { InformationCircleIcon } from '@heroicons/react/outline'
const historyViews = [
{ label: 'Trades', key: 'Trades' },
@ -39,9 +40,6 @@ export default function AccountHistory() {
const { t } = useTranslation('common')
const [view, setView] = useState('Trades')
const [history, setHistory] = useState(null)
const [loadExportData, setLoadExportData] = useState(false)
const wallet = useMangoStore((s) => s.wallet.current)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoAccountPk = useMemo(() => {
@ -64,68 +62,6 @@ export default function AccountHistory() {
}
}, [mangoAccountPk])
const exportPerformanceDataToCSV = async () => {
setLoadExportData(true)
const exportData = await fetchHourlyPerformanceStats(
mangoAccount.publicKey.toString(),
10000
)
const dataToExport = exportData.map((row) => {
const timestamp = new Date(row.time)
return {
timestamp: `${timestamp.toLocaleDateString()} ${timestamp.toLocaleTimeString()}`,
account_equity: row.account_equity,
pnl: row.pnl,
}
})
const title = `${
mangoAccount.name || mangoAccount.publicKey
}-Performance-${new Date().toLocaleDateString()}`
const headers = ['Timestamp', 'Account Equity', 'PNL']
exportDataToCSV(dataToExport, title, headers, t)
setLoadExportData(false)
}
const exportHistoryToCSV = () => {
const dataToExport = history
.filter((val) => val.activity_type == view)
.map((row) => {
row = row.activity_details
const timestamp = new Date(row.block_datetime)
return {
date: `${timestamp.toLocaleDateString()} ${timestamp.toLocaleTimeString()}`,
asset: row.symbol,
quantity: row.quantity,
value: row.usd_equivalent,
}
})
const headers = ['Timestamp', 'Asset', 'Quantity', 'Value']
if (dataToExport.length == 0) {
notify({
title: t('export-data-empty'),
description: '',
type: 'info',
})
return
}
const tab = historyViews.filter((v) => v.key == view)[0].label
const title = `${
mangoAccount.name || mangoAccount.publicKey
}-${tab}-${new Date().toLocaleDateString()}`
exportDataToCSV(dataToExport, title, headers, t)
}
const canWithdraw =
mangoAccount && wallet?.publicKey
? mangoAccount.owner.equals(wallet.publicKey)
: false
return (
<>
<div className="mb-4 flex rounded-md bg-th-bkg-3 px-3 py-2 md:mb-6 md:px-4">
@ -147,70 +83,6 @@ export default function AccountHistory() {
</div>
))}
</div>
<div className="flex flex-col pb-6 sm:flex-row sm:items-end sm:justify-between">
<div className="pb-4 sm:pb-0">
<h2 className="mb-1">{t(`${view.toLowerCase()}-history`)}</h2>
<div className="mr-4 text-xs text-th-fgd-3">
{t('delay-displaying-recent')} {t('use-explorer-one')}
<a
href={`https://explorer.solana.com/address/${mangoAccountPk}`}
target="_blank"
rel="noopener noreferrer"
>
{t('use-explorer-two')}
</a>
{t('use-explorer-three')}
</div>
</div>
<div className="flex space-x-3">
{view === 'Trades' && canWithdraw ? (
<Button
className={`flex h-8 items-center justify-center whitespace-nowrap pt-0 pb-0 pl-3 pr-3 text-xs`}
onClick={exportPerformanceDataToCSV}
>
{loadExportData ? (
<Loading />
) : (
<div className={`flex items-center`}>
<SaveIcon className={`mr-1.5 h-4 w-4`} />
{t('export-pnl-csv')}
</div>
)}
</Button>
) : null}
{view !== 'Trades' ? (
<Button
className={`flex h-8 items-center justify-center whitespace-nowrap pt-0 pb-0 pl-3 pr-3 text-xs`}
onClick={exportHistoryToCSV}
>
<div className={`flex items-center`}>
<SaveIcon className={`mr-1.5 h-4 w-4`} />
{t('export-data')}
</div>
</Button>
) : canWithdraw ? (
<div className={`flex items-center`}>
<a
className={`default-transition flex h-8 items-center justify-center whitespace-nowrap rounded-full bg-th-bkg-button pt-0 pb-0 pl-3 pr-3 text-xs font-bold text-th-fgd-1 hover:text-th-fgd-1 hover:brightness-[1.1]`}
href={`https://event-history-api.herokuapp.com/all_trades_csv?mango_account=${mangoAccountPk}&open_orders=${mangoAccount.spotOpenOrders
.filter(
(e) => e.toString() !== '11111111111111111111111111111111'
)
.join(',')}`}
download
target="_blank"
rel="noopener noreferrer"
>
<SaveIcon className={`mr-1.5 h-4 w-4`} />
Export Trades CSV
</a>
<Tooltip content={t('trade-export-disclaimer')}>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-3" />
</Tooltip>
</div>
) : null}
</div>
</div>
<ViewContent view={view} history={history} />
</>
)
@ -219,7 +91,7 @@ export default function AccountHistory() {
const ViewContent = ({ view, history }) => {
switch (view) {
case 'Trades':
return <TradeHistoryTable />
return <TradeHistoryTable showExportPnl />
case 'Deposit':
return <HistoryTable history={history} view={view} />
case 'Withdraw':
@ -227,7 +99,7 @@ const ViewContent = ({ view, history }) => {
case 'Liquidation':
return <LiquidationHistoryTable history={history} view={view} />
default:
return <TradeHistoryTable />
return <TradeHistoryTable showExportPnl />
}
}
@ -291,6 +163,7 @@ const parseActivityDetails = (activity_details, activity_type, perpMarket) => {
const LiquidationHistoryTable = ({ history, view }) => {
const { t } = useTranslation('common')
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const markets = useMangoStore((s) => s.selectedMangoGroup.markets)
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const filteredHistory = useMemo(() => {
@ -300,8 +173,75 @@ const LiquidationHistoryTable = ({ history, view }) => {
}, [history, view])
const { items, requestSort, sortConfig } = useSortableData(filteredHistory)
const exportHistoryToCSV = () => {
const dataToExport = history
.filter((val) => val.activity_type == view)
.map((row) => {
row = row.activity_details
const timestamp = new Date(row.block_datetime)
return {
date: `${timestamp.toLocaleDateString()} ${timestamp.toLocaleTimeString()}`,
asset: row.symbol,
quantity: row.quantity,
value: row.usd_equivalent,
}
})
const headers = ['Timestamp', 'Asset', 'Quantity', 'Value']
if (dataToExport.length == 0) {
notify({
title: t('export-data-empty'),
description: '',
type: 'info',
})
return
}
const tab = historyViews.filter((v) => v.key == view)[0].label
const title = `${
mangoAccount.name || mangoAccount.publicKey
}-${tab}-${new Date().toLocaleDateString()}`
exportDataToCSV(dataToExport, title, headers, t)
}
return (
<>
<div className="flex items-center justify-between pb-3">
<div className="flex items-center">
<h4 className="mb-0 text-th-fgd-1">
{filteredHistory.length}{' '}
{filteredHistory.length === 1 ? view : `${view}s`}
</h4>
<Tooltip
content={
<div className="mr-4 text-xs text-th-fgd-3">
{t('delay-displaying-recent')} {t('use-explorer-one')}
<a
href={`https://explorer.solana.com/address/${mangoAccount.publicKey.toString()}`}
target="_blank"
rel="noopener noreferrer"
>
{t('use-explorer-two')}
</a>
{t('use-explorer-three')}
</div>
}
>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
</Tooltip>
</div>
<Button
className={`flex h-8 items-center justify-center whitespace-nowrap pt-0 pb-0 pl-3 pr-3 text-xs`}
onClick={exportHistoryToCSV}
>
<div className={`flex items-center`}>
<SaveIcon className={`mr-1.5 h-4 w-4`} />
{t('export-data')}
</div>
</Button>
</div>
{items.length ? (
<>
<Table>
@ -312,7 +252,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('block_datetime')}
>
{t('date')}
<span className="font-normal">{t('date')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'block_datetime'
@ -330,7 +270,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('asset_amount')}
>
Asset Lost
<span className="font-normal">Asset Lost</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'asset_amount'
@ -347,7 +287,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('asset_price')}
>
Price
<span className="font-normal">Price</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'asset_price'
@ -365,7 +305,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('liab_amount')}
>
Asset Gained
<span className="font-normal">Asset Gained</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'liab_amount'
@ -382,7 +322,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('liab_price')}
>
Price
<span className="font-normal">Price</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'liab_price'
@ -487,6 +427,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
const HistoryTable = ({ history, view }) => {
const { t } = useTranslation('common')
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const filteredHistory = useMemo(() => {
return history?.length
? history
@ -496,8 +437,81 @@ const HistoryTable = ({ history, view }) => {
}, [history, view])
const { items, requestSort, sortConfig } = useSortableData(filteredHistory)
const exportHistoryToCSV = () => {
const dataToExport = history
.filter((val) => val.activity_type == view)
.map((row) => {
row = row.activity_details
const timestamp = new Date(row.block_datetime)
return {
date: `${timestamp.toLocaleDateString()} ${timestamp.toLocaleTimeString()}`,
asset: row.symbol,
quantity: row.quantity,
value: row.usd_equivalent,
}
})
const headers = ['Timestamp', 'Asset', 'Quantity', 'Value']
if (dataToExport.length == 0) {
notify({
title: t('export-data-empty'),
description: '',
type: 'info',
})
return
}
const tab = historyViews.filter((v) => v.key == view)[0].label
const title = `${
mangoAccount.name || mangoAccount.publicKey
}-${tab}-${new Date().toLocaleDateString()}`
exportDataToCSV(dataToExport, title, headers, t)
}
return (
<>
<div className="flex items-center justify-between pb-3">
<div className="flex items-center">
<h4 className="mb-0 text-th-fgd-1">
{filteredHistory.length}{' '}
{filteredHistory.length === 1
? view === 'Withdraw'
? 'Withdrawal'
: view
: view === 'Withdraw'
? 'Withdrawals'
: `${view}s`}
</h4>
<Tooltip
content={
<div className="mr-4 text-xs text-th-fgd-3">
{t('delay-displaying-recent')} {t('use-explorer-one')}
<a
href={`https://explorer.solana.com/address/${mangoAccount.publicKey.toString()}`}
target="_blank"
rel="noopener noreferrer"
>
{t('use-explorer-two')}
</a>
{t('use-explorer-three')}
</div>
}
>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
</Tooltip>
</div>
<Button
className={`flex h-8 items-center justify-center whitespace-nowrap pt-0 pb-0 pl-3 pr-3 text-xs`}
onClick={exportHistoryToCSV}
>
<div className={`flex items-center`}>
<SaveIcon className={`mr-1.5 h-4 w-4`} />
{t('export-data')}
</div>
</Button>
</div>
{items.length ? (
<>
<Table>
@ -508,7 +522,7 @@ const HistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('block_datetime')}
>
{t('date')}
<span className="font-normal">{t('date')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'block_datetime'
@ -525,7 +539,7 @@ const HistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('symbol')}
>
{t('asset')}
<span className="font-normal">{t('asset')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'symbol'
@ -542,7 +556,7 @@ const HistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('quantity')}
>
{t('quantity')}
<span className="font-normal">{t('quantity')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'quantity'
@ -559,7 +573,7 @@ const HistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('usd_equivalent')}
>
{t('value')}
<span className="font-normal">{t('value')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'usd_equivalent'

View File

@ -35,14 +35,15 @@ export const fetchHourlyPerformanceStats = async (
.format('YYYY-MM-DD')}`
)
const parsedResponse = await response.json()
const entries: any = Object.entries(parsedResponse)
const entries: any = Object.entries(parsedResponse).sort((a, b) =>
b[0].localeCompare(a[0])
)
const stats = entries
.map(([key, value]) => {
return { ...value, time: key }
})
.filter((x) => x)
.reverse()
return stats
}
@ -183,7 +184,7 @@ export default function AccountOverview() {
<h2 className="mb-4">{t('assets-liabilities')}</h2>
<div className="grid grid-flow-col grid-cols-1 grid-rows-2 pb-8 md:grid-cols-2 md:grid-rows-1 md:gap-4 md:pb-12">
<div className="rounded-md border-t border-th-bkg-4 p-3 sm:rounded-lg sm:p-4 md:border-b">
<div className="border-t border-th-bkg-4 p-3 sm:p-4 md:border-b">
<div className="pb-0.5 text-th-fgd-3">{t('total-assets')}</div>
<div className="flex items-center">
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
@ -193,7 +194,7 @@ export default function AccountOverview() {
</div>
</div>
</div>
<div className="rounded-md border-b border-t border-th-bkg-4 p-3 sm:rounded-lg sm:p-4">
<div className="border-b border-t border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-th-fgd-3">{t('total-liabilities')}</div>
<div className="flex items-center">
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">

View File

@ -74,14 +74,15 @@ const AccountPerformance = () => {
`https://mango-transaction-log.herokuapp.com/v3/stats/account-performance?mango-account=${mangoAccountPk}`
)
const parsedResponse = await response.json()
const entries: any = Object.entries(parsedResponse)
const entries: any = Object.entries(parsedResponse).sort((a, b) =>
b[0].localeCompare(a[0])
)
const stats = entries
.map(([key, value]) => {
return { ...value, time: key }
})
.filter((x) => x)
.reverse()
setLoading(false)
setHourlyPerformanceStats(stats)

View File

@ -0,0 +1,60 @@
import { PublicKey } from '@solana/web3.js'
import Button from 'components/Button'
import Input from 'components/Input'
import { useRouter } from 'next/router'
import React, { useState } from 'react'
import { ExclamationCircleIcon } from '@heroicons/react/outline'
import { useTranslation } from 'next-i18next'
export const MangoAccountLookup = () => {
const { t } = useTranslation('common')
const router = useRouter()
const [value, setValue] = useState('')
const [isInvalid, setIsInvalid] = useState(false)
const validatePubKey = (key: string) => {
try {
const pubkey = new PublicKey(key)
return PublicKey.isOnCurve(pubkey.toBuffer())
} catch (e) {
return false
}
}
const onClickSearch = () => {
const isValid = validatePubKey(value)
if (isValid) {
const route = `/account?pubkey=${value}`
setValue('')
router.push(route)
} else {
setIsInvalid(true)
}
}
return (
<div className="flex flex-col items-center rounded-lg px-4 text-th-fgd-1">
<h2 className="mb-1 text-base">{t('mango-account-lookup-title')}</h2>
<p className="mb-2 text-center">{t('mango-account-lookup-desc')}</p>
<div className="w-[350px] p-1 md:w-[400px]">
<Input
type="text"
error={isInvalid}
placeholder="Address"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</div>
{isInvalid && (
<div className="flex items-center pt-1.5 text-th-red">
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
{t('invalid-address')}
</div>
)}
<div className="pt-3 pb-2">
<Button onClick={onClickSearch}>{t('view')}</Button>
</div>
</div>
)
}

81
components/index.tsx Normal file
View File

@ -0,0 +1,81 @@
export * from './AccountInfo'
export * from './AccountNameModal'
export * from './AccountSelect'
export * from './AccountsModal'
export * from './AlphaModal'
export * from './BalancesTable'
export * from './Button'
export * from './ButtonGroup'
export * from './Chart'
export * from './Checkbox'
export * from './CloseAccountModal'
export * from './ConnectWalletButton'
export * from './CreateAlertModal'
export * from './DayHighLow'
export * from './DelegateModal'
export * from './DepositModal'
export * from './DepositMsrmModal'
export * from './DropMenu'
export * from './EmptyState'
export * from './ErrorBoundary'
export * from './FavoritesShortcutBar'
export * from './FlipCard'
export * from './FloatingElement'
export * from './GlobalNotification'
export * from './icons'
export * from './ImageWithFallback'
export * from './InlineNotification'
export * from './Input'
export * from './IntroTips'
export * from './JupiterForm'
export * from './LeverageSlider'
export * from './Loading'
export * from './MangoAccountSelect'
export * from './MangoIcon'
export * from './ManualRefresh'
export * from './MarketBalances'
export * from './MarketCloseModal'
export * from './MarketDetails'
export * from './MarketFee'
export * from './MarketMenuItem'
export * from './MarketNavItem'
export * from './MarketSelect'
export * from './MarketsModal'
export * from './MenuItem'
export * from './Modal'
export * from './NavDropMenu'
export * from './NewAccount'
export * from './Notification'
export * from './OpenOrdersTable'
export * from './Orderbook'
export * from './PageBodyContainer'
export * from './Pagination'
export * from './PerpPositionsTable'
export * from './PerpSideBadge'
export * from './PnlText'
export * from './RecentMarketTrades'
export * from './ResetLayout'
export * from './Select'
export * from './Settings'
export * from './SettingsModal'
export * from './SideBadge'
export * from './Slider'
export * from './styles'
export * from './SwapSettingsModal'
export * from './SwapTokenInfo'
export * from './SwapTokenInsights'
export * from './SwapTokenSelect'
export * from './Switch'
export * from './SwitchMarketDropdown'
export * from './TableElements'
export * from './Tabs'
export * from './Tooltip'
export * from './TopBar'
export * from './TradeHistoryTable'
export * from './TradeNavMenu'
export * from './TradePageGrid'
export * from './UiLock'
export * from './UserInfo'
export * from './UserMarketInfo'
export * from './WithdrawModal'
export * from './WithdrawMsrmModal'

View File

@ -1,8 +1,24 @@
import { ReactNode, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ChartBarIcon, CurrencyDollarIcon } from '@heroicons/react/solid'
import {
ChartBarIcon,
CurrencyDollarIcon,
MenuIcon,
XIcon,
} from '@heroicons/react/solid'
import { BtcMonoIcon, TradeIcon } from '../icons'
import { useTranslation } from 'next-i18next'
import { IconButton } from '../Button'
import {
CalculatorIcon,
CashIcon,
ChevronRightIcon,
CurrencyDollarIcon as FeesIcon,
LightBulbIcon,
SwitchHorizontalIcon,
UserAddIcon,
} from '@heroicons/react/outline'
const StyledBarItemLabel = ({ children, ...props }) => (
<div style={{ fontSize: '0.6rem', lineHeight: 1 }} {...props}>
@ -13,10 +29,11 @@ const StyledBarItemLabel = ({ children, ...props }) => (
const BottomBar = () => {
const { t } = useTranslation('common')
const { asPath } = useRouter()
const [showPanel, setShowPanel] = useState(false)
return (
<>
<div className="default-transition grid grid-cols-4 grid-rows-1 bg-th-bkg-3 py-2.5">
<div className="default-transition grid grid-cols-5 grid-rows-1 bg-th-bkg-3 py-2.5">
<Link
href={{
pathname: '/select',
@ -69,9 +86,114 @@ const BottomBar = () => {
<StyledBarItemLabel>{t('stats')}</StyledBarItemLabel>
</div>
</Link>
<div
className={`${
showPanel ? 'text-th-primary' : 'text-th-fgd-3'
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
onClick={() => setShowPanel(!showPanel)}
>
<MenuIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('more')}</StyledBarItemLabel>
</div>
</div>
<MoreMenuPanel showPanel={showPanel} setShowPanel={setShowPanel} />
</>
)
}
export default BottomBar
const MoreMenuPanel = ({
showPanel,
setShowPanel,
}: {
showPanel: boolean
setShowPanel: (showPanel: boolean) => void
}) => {
const { t } = useTranslation('common')
return (
<div
className={`fixed bottom-0 z-30 h-96 w-full transform overflow-hidden bg-th-bkg-4 px-4 transition-all duration-700 ease-in-out ${
showPanel ? 'translate-y-0' : 'translate-y-full'
}`}
>
<div className="flex justify-end py-4">
<IconButton className="" onClick={() => setShowPanel(false)}>
<XIcon className="h-5 w-5" />
</IconButton>
</div>
<div
className="border-b border-th-fgd-4"
onClick={() => setShowPanel(false)}
>
<MoreMenuItem
title={t('borrow')}
path="/borrow"
icon={<CashIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('calculator')}
path="/risk-calculator"
icon={<CalculatorIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('swap')}
path="/swap"
icon={<SwitchHorizontalIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('referrals')}
path="/referral"
icon={<UserAddIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('fees')}
path="/fees"
icon={<FeesIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('learn')}
path="https://docs.mango.markets/"
icon={<LightBulbIcon className="h-5 w-5" />}
isExternal
/>
</div>
</div>
)
}
const MoreMenuItem = ({
title,
path,
icon,
isExternal,
}: {
title: string
path: string
icon: ReactNode
isExternal?: boolean
}) =>
isExternal ? (
<a
className="default-transition flex w-full items-center justify-between border-t border-th-fgd-4 px-2 py-3 text-th-fgd-2 hover:text-th-fgd-1"
href={path}
target="_blank"
rel="noopener noreferrer"
>
<div className="flex items-center">
{icon}
<span className="ml-1.5">{title}</span>
</div>
<ChevronRightIcon className="h-5 w-5" />
</a>
) : (
<Link href={path} shallow={true}>
<a className="default-transition flex w-full items-center justify-between border-t border-th-fgd-4 px-2 py-3 text-th-fgd-2 hover:text-th-fgd-1">
<div className="flex items-center">
{icon}
<span className="ml-1.5">{title}</span>
</div>
<ChevronRightIcon className="h-5 w-5" />
</a>
</Link>
)

View File

@ -17,6 +17,7 @@ import FloatingElement from '../FloatingElement'
import Swipeable from './Swipeable'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import { useWallet } from '@solana/wallet-adapter-react'
const TVChartContainer = dynamic(
() => import('../../components/TradingView/index'),
@ -26,10 +27,10 @@ const TVChartContainer = dynamic(
const MobileTradePage = () => {
const { t } = useTranslation('common')
const [viewIndex, setViewIndex] = useState(0)
const { connected } = useWallet()
const selectedMarket = useMangoStore((s) => s.selectedMarket.current)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const connected = useMangoStore((s) => s.wallet.connected)
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const baseSymbol = marketConfig.baseSymbol
const isPerpMarket = marketConfig.kind === 'perp'

View File

@ -15,29 +15,32 @@ function formatNumberString(x: number, decimals): string {
}
const getAverageStats = (
stats,
stats: any[],
daysAgo: number,
symbol: string,
type: string
) => {
const priorDate = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
const selectedStatsData = stats.filter((s) => s.name === symbol)
const timeFilteredStats = selectedStatsData.filter(
(d) => new Date(d.time).getTime() >= priorDate.getTime()
)
): string => {
if (stats?.length) {
const priorDate = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
const selectedStatsData = stats.filter((s) => s.name === symbol)
const timeFilteredStats = selectedStatsData.filter(
(d) => new Date(d.time).getTime() >= priorDate.getTime()
)
const oldestStat = timeFilteredStats[0]
const latestStat = timeFilteredStats[timeFilteredStats.length - 1]
const avg =
Math.pow(latestStat[type] / oldestStat[type], 365 / daysAgo) * 100 - 100
const oldestStat = timeFilteredStats[0]
const latestStat = timeFilteredStats[timeFilteredStats.length - 1]
const avg =
Math.pow(latestStat[type] / oldestStat[type], 365 / daysAgo) * 100 - 100
priorDate.setHours(priorDate.getHours() + 1)
priorDate.setHours(priorDate.getHours() + 1)
if (new Date(oldestStat.hourly).getDate() > priorDate.getDate()) {
return '-'
} else {
return `${avg.toFixed(4)}%`
if (new Date(oldestStat.hourly).getDate() > priorDate.getDate()) {
return '-'
} else {
return `${avg.toFixed(4)}%`
}
}
return '-'
}
export default function StatsTotals({ latestStats, stats }) {

View File

@ -42,6 +42,7 @@ import useLocalStorageState, {
} from '../../hooks/useLocalStorageState'
import InlineNotification from '../InlineNotification'
import { DEFAULT_SPOT_MARGIN_KEY } from '../SettingsModal'
import { useWallet } from '@solana/wallet-adapter-react'
const MAX_SLIPPAGE_KEY = 'maxSlippage'
@ -61,7 +62,7 @@ export default function AdvancedTradeForm({
const { t } = useTranslation('common')
const set = useMangoStore((s) => s.set)
const { ipAllowed, spotAllowed } = useIpAddress()
const connected = useMangoStore((s) => s.wallet.connected)
const { wallet, connected } = useWallet()
const actions = useMangoStore((s) => s.actions)
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
@ -113,10 +114,7 @@ export default function AdvancedTradeForm({
const isTriggerOrder = TRIGGER_ORDER_TYPES.includes(tradeType)
// TODO saml - create a tick box on the UI; Only available on perps
// eslint-disable-next-line
const [postOnlySlide, setPostOnlySlide] = useState(false)
const [postOnly, setPostOnly] = useState(false)
const [ioc, setIoc] = useState(false)
@ -405,8 +403,6 @@ export default function AdvancedTradeForm({
}
}
// TODO saml - use
// eslint-disable-next-line
const postOnlySlideOnChange = (checked) => {
if (checked) {
setIoc(false)
@ -414,7 +410,6 @@ export default function AdvancedTradeForm({
}
setPostOnlySlide(checked)
}
const postOnChange = (checked) => {
if (checked) {
setIoc(false)
@ -575,7 +570,6 @@ export default function AdvancedTradeForm({
useMangoStore.getState().accountInfos[marketConfig.asksKey.toString()]
const bidInfo =
useMangoStore.getState().accountInfos[marketConfig.bidsKey.toString()]
const wallet = useMangoStore.getState().wallet.current
const referrerPk = useMangoStore.getState().referrerPk
if (!wallet || !mangoGroup || !mangoAccount || !market) return
@ -618,7 +612,7 @@ export default function AdvancedTradeForm({
mangoGroup,
mangoAccount,
market,
wallet,
wallet?.adapter,
side,
orderPrice,
baseSize,
@ -632,7 +626,9 @@ export default function AdvancedTradeForm({
let perpOrderPrice: number = orderPrice
if (isMarketOrder) {
if (tradeType === 'Market' && maxSlippage !== undefined) {
if (postOnlySlide) {
perpOrderType = 'postOnlySlide'
} else if (tradeType === 'Market' && maxSlippage !== undefined) {
perpOrderType = 'ioc'
if (side === 'buy') {
perpOrderPrice = markPrice * (1 + parseFloat(maxSlippage))
@ -652,7 +648,7 @@ export default function AdvancedTradeForm({
mangoGroup,
mangoAccount,
market,
wallet,
wallet?.adapter,
perpOrderType,
side,
perpOrderPrice,
@ -667,7 +663,7 @@ export default function AdvancedTradeForm({
mangoGroup,
mangoAccount,
market,
wallet,
wallet?.adapter,
side,
perpOrderPrice,
baseSize,
@ -793,15 +789,19 @@ export default function AdvancedTradeForm({
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={isMarketOrder}
value={postOnlySlide ? '' : price}
disabled={isMarketOrder || postOnlySlide}
placeholder={tradeType === 'Market' ? markPrice : null}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
<>
{!postOnlySlide && (
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
)}
</>
}
/>
</>
@ -908,7 +908,7 @@ export default function AdvancedTradeForm({
</div>
) : null
) : null}
<div className="sm:flex">
<div className="flex-wrap sm:flex">
{isLimitOrder ? (
<div className="flex">
<div className="mr-4 mt-3">
@ -951,7 +951,7 @@ export default function AdvancedTradeForm({
&& showReduceOnly(perpAccount?.basePosition.toNumber())
*/}
{marketConfig.kind === 'perp' ? (
<div className="mt-3">
<div className="mr-4 mt-3">
<Tooltip
className="hidden md:block"
delay={250}
@ -968,6 +968,24 @@ export default function AdvancedTradeForm({
</Tooltip>
</div>
) : null}
{marketConfig.kind === 'perp' ? (
<div className="mt-3">
<Tooltip
className="hidden md:block"
delay={250}
placement="left"
content={t('tooltip-post-and-slide')}
>
<Checkbox
checked={postOnlySlide}
onChange={(e) => postOnlySlideOnChange(e.target.checked)}
disabled={isTriggerOrder}
>
Post & Slide
</Checkbox>
</Tooltip>
</div>
) : null}
{marketConfig.kind === 'spot' ? (
<div className="mt-3">
<Tooltip

View File

@ -23,12 +23,13 @@ import OrderSideTabs from './OrderSideTabs'
import Tooltip from '../Tooltip'
import EstPriceImpact from './EstPriceImpact'
import { useTranslation } from 'next-i18next'
import { useWallet } from '@solana/wallet-adapter-react'
export default function SimpleTradeForm({ initLeverage }) {
const { t } = useTranslation('common')
const set = useMangoStore((s) => s.set)
const { ipAllowed, spotAllowed } = useIpAddress()
const connected = useMangoStore((s) => s.wallet.connected)
const { wallet, connected } = useWallet()
const actions = useMangoStore((s) => s.actions)
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
@ -277,9 +278,8 @@ export default function SimpleTradeForm({ initLeverage }) {
useMangoStore.getState().accountInfos[marketConfig.asksKey.toString()]
const bidInfo =
useMangoStore.getState().accountInfos[marketConfig.bidsKey.toString()]
const wallet = useMangoStore.getState().wallet.current
if (!wallet || !mangoGroup || !mangoAccount || !market) return
if (!wallet?.adapter || !mangoGroup || !mangoAccount || !market) return
setSubmitting(true)
try {
@ -308,7 +308,7 @@ export default function SimpleTradeForm({ initLeverage }) {
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
wallet?.adapter,
side,
orderPrice,
baseSize,
@ -319,7 +319,7 @@ export default function SimpleTradeForm({ initLeverage }) {
mangoGroup,
mangoAccount,
market,
wallet,
wallet?.adapter,
side,
orderPrice,
baseSize,

View File

@ -11,12 +11,13 @@ import {
FlipCardInner,
} from '../FlipCard'
import FloatingElement from '../FloatingElement'
import { useWallet } from '@solana/wallet-adapter-react'
export default function TradeForm() {
const [showAdvancedForm, setShowAdvancedForm] = useState(true)
const { connected } = useWallet()
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const connected = useMangoStore((s) => s.wallet.connected)
const handleFormChange = () => {
setShowAdvancedForm(!showAdvancedForm)

33
hooks/useFilteredData.tsx Normal file
View File

@ -0,0 +1,33 @@
import { useMemo } from 'react'
interface Filters {
[key: string]: any
value?: any
}
export function useFilteredData<T>(items: T[], filters: Filters) {
const filteredItems = useMemo(() => {
const filtered = items.filter((item) => {
let validItem = true
for (const [key, value] of Object.entries(filters)) {
if (Array.isArray(value)) {
if (!value.includes(item[key]) && value.length > 0) {
validItem = false
}
} else if (Object.prototype.hasOwnProperty.call(value, 'condition')) {
if (!value.condition(item[key])) {
validItem = false
}
} else {
if (value !== item[key]) validItem = false
}
}
return validItem
})
return filtered
}, [items])
return filteredItems
}

View File

@ -39,19 +39,23 @@ export const collectPerpPosition = (
let avgEntryPrice = 0,
breakEvenPrice = 0
const perpTradeHistory = tradeHistory.filter(
(t) => t.marketName === marketConfig.name
)
try {
const perpTradeHistory = tradeHistory.filter(
(t) => t.marketName === marketConfig.name
)
avgEntryPrice = perpAccount
.getAverageOpenPrice(mangoAccount, perpMarket, perpTradeHistory)
.toNumber()
} catch (e) {
console.error(marketConfig.name, e)
}
try {
breakEvenPrice = perpAccount
.getBreakEvenPrice(mangoAccount, perpMarket, perpTradeHistory)
.toNumber()
} catch (e) {
// console.error(e)
console.error(marketConfig.name, e)
}
const basePosition = perpMarket?.baseLotsToNumber(perpAccount.basePosition)
@ -91,7 +95,7 @@ const usePerpPositions = () => {
const mangoGroup = useMangoStore(mangoGroupSelector)
const mangoCache = useMangoStore(mangoCacheSelector)
const allMarkets = useMangoStore(marketsSelector)
const tradeHistory = useMangoStore.getState().tradeHistory.parsed
const tradeHistory = useMangoStore((s) => s.tradeHistory.parsed)
useEffect(() => {
if (

View File

@ -1,103 +0,0 @@
import { useEffect, useMemo } from 'react'
import Wallet from '@project-serum/sol-wallet-adapter'
import useLocalStorageState from './useLocalStorageState'
import useMangoStore from '../stores/useMangoStore'
import { notify } from '../utils/notifications'
import { WalletAdapter } from '../@types/types'
import { useTranslation } from 'next-i18next'
import { DEFAULT_PROVIDER, WALLET_PROVIDERS } from '../utils/wallet-adapters'
export const PROVIDER_LOCAL_STORAGE_KEY = 'walletProvider-0.1'
export default function useWallet() {
const { t } = useTranslation('common')
const setMangoStore = useMangoStore((state) => state.set)
const {
current: wallet,
connected,
providerUrl: selectedProviderUrl,
} = useMangoStore((state) => state.wallet)
const endpoint = useMangoStore((state) => state.connection.endpoint)
const actions = useMangoStore((s) => s.actions)
const [savedProviderUrl, setSavedProviderUrl] = useLocalStorageState(
PROVIDER_LOCAL_STORAGE_KEY,
DEFAULT_PROVIDER.url
)
const provider = useMemo(
() => WALLET_PROVIDERS.find(({ url }) => url === savedProviderUrl),
[savedProviderUrl]
)
useEffect(() => {
if (selectedProviderUrl) {
setSavedProviderUrl(selectedProviderUrl)
}
}, [selectedProviderUrl])
useEffect(() => {
if (provider) {
const updateWallet = () => {
// hack to also update wallet synchronously in case it disconnects
// eslint-disable-next-line react-hooks/exhaustive-deps
const wallet = new (provider.adapter || Wallet)(
savedProviderUrl,
endpoint
) as WalletAdapter
setMangoStore((state) => {
state.wallet.current = wallet
})
}
if (document.readyState !== 'complete') {
// wait to ensure that browser extensions are loaded
const listener = () => {
updateWallet()
window.removeEventListener('load', listener)
}
window.addEventListener('load', listener)
return () => window.removeEventListener('load', listener)
} else {
updateWallet()
}
}
}, [provider, savedProviderUrl, endpoint])
useEffect(() => {
if (!wallet) return
wallet.on('connect', async () => {
setMangoStore((state) => {
state.wallet.connected = true
})
// set connected before fetching data
await actions.fetchAllMangoAccounts()
actions.fetchProfilePicture()
actions.reloadOrders()
actions.fetchTradeHistory()
actions.fetchWalletTokens()
// notify({
// title: t('wallet-connected'),
// description:
// t('connected-to') +
// wallet.publicKey.toString().substr(0, 5) +
// '...' +
// wallet.publicKey.toString().substr(-5),
// })
})
wallet.on('disconnect', () => {
console.log('disconnecting wallet')
setMangoStore((state) => {
state.wallet.connected = false
state.mangoAccounts = []
state.selectedMangoAccount.current = null
state.tradeHistory = { spot: [], perp: [], parsed: [] }
})
notify({
type: 'info',
title: t('wallet-disconnected'),
})
})
}, [wallet, setMangoStore])
return { connected, wallet }
}

View File

@ -1,6 +0,0 @@
[build]
command = "yarn build"
publish = ".next"
[functions]
included_files = ["public/locales/*/*.json", "next-i18next.config.js"]

View File

@ -21,34 +21,29 @@ const moduleExports = {
},
]
},
webpack: (config, { isServer }) => {
webpack: (config, options) => {
// Important: return the modified config
if (!isServer) {
if (!options.isServer) {
config.resolve.fallback.fs = false
}
config.module.rules.push({
test: /\.svg?$/,
oneOf: [
{
use: [
{
loader: '@svgr/webpack',
options: {
prettier: false,
svgo: true,
svgoConfig: {
plugins: [{ removeViewBox: false }],
},
titleProp: true,
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
],
test: /\.svg$/,
use: ['@svgr/webpack'],
})
if (process.env.ANALYZE === 'true') {
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: options.isServer
? './analyze/server.html'
: './analyze/client.html',
})
)
}
return config
},
}

View File

@ -13,17 +13,27 @@
"format": "prettier --write .",
"lint": "eslint . --ext ts --ext tsx --ext js --quiet",
"lint-staged": "lint-staged --config lint-staged.js",
"test": "jest",
"test-all": "yarn lint && yarn type-check && yarn test"
"lint-all": "yarn lint && yarn type-check",
"analyze": "ANALYZE=true yarn build"
},
"dependencies": {
"@blockworks-foundation/mango-client": "^3.3.25",
"@blockworks-foundation/mango-client": "^3.3.27",
"@headlessui/react": "^0.0.0-insiders.2dbc38c",
"@heroicons/react": "^1.0.0",
"@jup-ag/react-hook": "^1.0.0-beta.16",
"@next/bundle-analyzer": "^12.1.0",
"@project-serum/serum": "0.13.55",
"@project-serum/sol-wallet-adapter": "0.2.0",
"@solana/web3.js": "^1.31.0",
"@solana/wallet-adapter-base": "^0.9.5",
"@solana/wallet-adapter-bitpie": "^0.5.3",
"@solana/wallet-adapter-glow": "^0.1.1",
"@solana/wallet-adapter-huobi": "^0.1.0",
"@solana/wallet-adapter-phantom": "^0.9.3",
"@solana/wallet-adapter-react": "^0.15.4",
"@solana/wallet-adapter-slope": "^0.5.4",
"@solana/wallet-adapter-solflare": "^0.6.6",
"@solana/wallet-adapter-sollet": "^0.11.1",
"@solana/web3.js": "^1.36.0",
"@solflare-wallet/pfp": "^0.0.6",
"@solflare-wallet/sdk": "^1.0.10",
"@tippyjs/react": "^4.2.5",
@ -31,7 +41,6 @@
"bignumber.js": "^9.0.2",
"bn.js": "^5.1.0",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.0",
"dayjs": "^1.10.4",
"export-to-csv": "^0.2.1",
"html2canvas": "^1.4.1",
@ -45,6 +54,7 @@
"rc-slider": "^9.7.5",
"react": "^17.0.2",
"react-cool-dimensions": "^2.0.7",
"react-datepicker": "^4.7.0",
"react-dom": "^17.0.2",
"react-grid-layout": "^1.3.3",
"react-portal": "^4.2.1",
@ -57,10 +67,8 @@
"zustand": "^3.7.0"
},
"devDependencies": {
"@netlify/plugin-nextjs": "^4.1.0",
"@svgr/webpack": "^6.1.2",
"@testing-library/react": "^11.2.5",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.25",
"@types/react": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
@ -72,8 +80,6 @@
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^7.0.4",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.4.7",
"jest-watch-typeahead": "^0.6.1",
"lint-staged": "^12.3.6",
"postcss": "^8.4.7",
"prettier": "^2.0.2",
@ -88,5 +94,10 @@
"bn.js": "5.1.3",
"@solana/buffer-layout": "3.0.0",
"@types/bn.js": "5.1.0"
},
"nextBundleAnalysis": {
"budget": 512000,
"budgetPercentIncreaseRed": 20,
"showDetails": true
}
}

View File

@ -2,9 +2,10 @@ import Head from 'next/head'
import { ThemeProvider } from 'next-themes'
import '../node_modules/react-grid-layout/css/styles.css'
import '../node_modules/react-resizable/css/styles.css'
import '../node_modules/react-datepicker/dist/react-datepicker.css'
import 'intro.js/introjs.css'
import '../styles/index.css'
import useWallet from '../hooks/useWallet'
import '../styles/datepicker.css'
import useHydrateStore from '../hooks/useHydrateStore'
import Notifications from '../components/Notification'
import useMangoStore from '../stores/useMangoStore'
@ -18,7 +19,7 @@ import ErrorBoundary from '../components/ErrorBoundary'
import GlobalNotification from '../components/GlobalNotification'
import { useOpenOrders } from '../hooks/useOpenOrders'
import usePerpPositions from '../hooks/usePerpPositions'
import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'
import { PublicKey } from '@solana/web3.js'
import { connectionSelector, mangoGroupSelector } from '../stores/selectors'
import {
@ -26,17 +27,20 @@ import {
ReferrerIdRecord,
} from '@blockworks-foundation/mango-client'
import useTradeHistory from '../hooks/useTradeHistory'
import { WalletProvider, WalletListener } from 'components/WalletAdapter'
import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom'
import { SolflareWalletAdapter } from '@solana/wallet-adapter-solflare'
import { SolletWalletAdapter } from '@solana/wallet-adapter-sollet'
import { SlopeWalletAdapter } from '@solana/wallet-adapter-slope'
import { BitpieWalletAdapter } from '@solana/wallet-adapter-bitpie'
import { HuobiWalletAdapter } from '@solana/wallet-adapter-huobi'
import { GlowWalletAdapter } from '@solana/wallet-adapter-glow'
const MangoStoreUpdater = () => {
useHydrateStore()
return null
}
const WalletStoreUpdater = () => {
useWallet()
return null
}
const OpenOrdersStoreUpdater = () => {
useOpenOrders()
return null
@ -54,9 +58,9 @@ const TradeHistoryStoreUpdater = () => {
const FetchReferrer = () => {
const setMangoStore = useMangoStore((s) => s.set)
const router = useRouter()
const mangoGroup = useMangoStore(mangoGroupSelector)
const connection = useMangoStore(connectionSelector)
const router = useRouter()
const { query } = router
useEffect(() => {
@ -120,6 +124,19 @@ const PageTitle = () => {
}
function App({ Component, pageProps }) {
const wallets = useMemo(
() => [
new PhantomWalletAdapter(),
new SolflareWalletAdapter(),
new SolletWalletAdapter(),
new SlopeWalletAdapter(),
new BitpieWalletAdapter(),
new HuobiWalletAdapter(),
new GlowWalletAdapter(),
],
[]
)
return (
<>
<Head>
@ -159,7 +176,6 @@ function App({ Component, pageProps }) {
<ErrorBoundary>
<PageTitle />
<MangoStoreUpdater />
<WalletStoreUpdater />
<OpenOrdersStoreUpdater />
<PerpPositionsStoreUpdater />
<TradeHistoryStoreUpdater />
@ -167,21 +183,24 @@ function App({ Component, pageProps }) {
</ErrorBoundary>
<ThemeProvider defaultTheme="Mango">
<ViewportProvider>
<div className="min-h-screen bg-th-bkg-1">
<ErrorBoundary>
<GlobalNotification />
<Component {...pageProps} />
</ErrorBoundary>
</div>
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
<ErrorBoundary>
<BottomBar />
</ErrorBoundary>
</div>
<WalletProvider wallets={wallets}>
<WalletListener />
<ViewportProvider>
<div className="min-h-screen bg-th-bkg-1">
<ErrorBoundary>
<GlobalNotification />
<Component {...pageProps} />
</ErrorBoundary>
</div>
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
<ErrorBoundary>
<BottomBar />
</ErrorBoundary>
</div>
<Notifications />
</ViewportProvider>
<Notifications />
</ViewportProvider>
</WalletProvider>
</ThemeProvider>
</ErrorBoundary>
</>

View File

@ -19,42 +19,41 @@ import {
} from '@heroicons/react/outline'
import { ChevronDownIcon } from '@heroicons/react/solid'
import { nativeToUi, ZERO_BN } from '@blockworks-foundation/mango-client'
import useMangoStore, {
serumProgramId,
MNGO_INDEX,
} from '../stores/useMangoStore'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import AccountOrders from '../components/account_page/AccountOrders'
import AccountHistory from '../components/account_page/AccountHistory'
import AccountsModal from '../components/AccountsModal'
import AccountOverview from '../components/account_page/AccountOverview'
import AccountInterest from '../components/account_page/AccountInterest'
import AccountFunding from '../components/account_page/AccountFunding'
import AccountNameModal from '../components/AccountNameModal'
import { IconButton, LinkButton } from '../components/Button'
import EmptyState from '../components/EmptyState'
import Loading from '../components/Loading'
import Swipeable from '../components/mobile/Swipeable'
import Tabs from '../components/Tabs'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from '../components/TradePageGrid'
import useMangoStore, { serumProgramId, MNGO_INDEX } from 'stores/useMangoStore'
import PageBodyContainer from 'components/PageBodyContainer'
import TopBar from 'components/TopBar'
import AccountOrders from 'components/account_page/AccountOrders'
import AccountHistory from 'components/account_page/AccountHistory'
import AccountsModal from 'components/AccountsModal'
import AccountOverview from 'components/account_page/AccountOverview'
import AccountInterest from 'components/account_page/AccountInterest'
import AccountFunding from 'components/account_page/AccountFunding'
import AccountNameModal from 'components/AccountNameModal'
import { IconButton, LinkButton } from 'components/Button'
import EmptyState from 'components/EmptyState'
import Loading from 'components/Loading'
import Swipeable from 'components/mobile/Swipeable'
import Tabs from 'components/Tabs'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'components/TradePageGrid'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useTranslation } from 'next-i18next'
import { useRouter } from 'next/router'
import { PublicKey } from '@solana/web3.js'
import CloseAccountModal from '../components/CloseAccountModal'
import { notify } from '../utils/notifications'
import CloseAccountModal from 'components/CloseAccountModal'
import { notify } from 'utils/notifications'
import {
actionsSelector,
mangoAccountSelector,
mangoGroupSelector,
walletConnectedSelector,
} from '../stores/selectors'
import CreateAlertModal from '../components/CreateAlertModal'
import { copyToClipboard } from '../utils'
import DelegateModal from '../components/DelegateModal'
} from 'stores/selectors'
import CreateAlertModal from 'components/CreateAlertModal'
import { copyToClipboard } from 'utils'
import DelegateModal from 'components/DelegateModal'
import { Menu, Transition } from '@headlessui/react'
import { useWallet } from '@solana/wallet-adapter-react'
import { handleWalletConnect } from 'components/ConnectWalletButton'
import { MangoAccountLookup } from 'components/account_page/MangoAccountLookup'
export async function getStaticProps({ locale }) {
return {
@ -76,15 +75,12 @@ export default function Account() {
const { t } = useTranslation(['common', 'close-account', 'delegate'])
const { width } = useViewport()
const router = useRouter()
const connected = useMangoStore(walletConnectedSelector)
const { connected, wallet, publicKey } = useWallet()
const isLoading = useMangoStore((s) => s.selectedMangoAccount.initialLoad)
const mangoAccount = useMangoStore(mangoAccountSelector)
const mangoGroup = useMangoStore(mangoGroupSelector)
const wallet = useMangoStore((s) => s.wallet.current)
const isLoading = useMangoStore((s) => s.selectedMangoAccount.initialLoad)
const actions = useMangoStore(actionsSelector)
const setMangoStore = useMangoStore((s) => s.set)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [showNameModal, setShowNameModal] = useState(false)
const [showCloseAccountModal, setShowCloseAccountModal] = useState(false)
@ -96,10 +92,11 @@ export default function Account() {
const [viewIndex, setViewIndex] = useState(0)
const [activeTab, setActiveTab] = useState(TABS[0])
const connecting = wallet?.adapter?.connecting
const isMobile = width ? width < breakpoints.sm : false
const { pubkey } = router.query
const isDelegatedAccount = wallet?.publicKey
? !mangoAccount?.owner?.equals(wallet?.publicKey)
const isDelegatedAccount = publicKey
? !mangoAccount?.owner?.equals(publicKey)
: false
const handleCloseAlertModal = useCallback(() => {
@ -122,6 +119,10 @@ export default function Account() {
setShowDelegateModal(false)
}, [])
const handleConnect = useCallback(() => {
handleWalletConnect(wallet)
}, [wallet])
useEffect(() => {
async function loadUnownedMangoAccount() {
try {
@ -140,6 +141,7 @@ export default function Account() {
setResetOnLeave(true)
}
} catch (error) {
console.log('error', error)
router.push('/account')
}
}
@ -152,12 +154,6 @@ export default function Account() {
}
}, [pubkey, mangoGroup])
useEffect(() => {
if (connected) {
router.push('/account')
}
}, [connected])
useEffect(() => {
const handleRouteChange = () => {
if (resetOnLeave) {
@ -205,8 +201,13 @@ export default function Account() {
)
}, [mangoAccount])
useEffect(() => {
if (connecting) {
router.push('/account')
}
}, [connecting, router])
const handleRedeemMngo = async () => {
const wallet = useMangoStore.getState().wallet.current
const mangoClient = useMangoStore.getState().connection.client
const mngoNodeBank =
mangoGroup.rootBankAccounts[MNGO_INDEX].nodeBankAccounts[0]
@ -215,7 +216,7 @@ export default function Account() {
const txid = await mangoClient.redeemAllMngo(
mangoGroup,
mangoAccount,
wallet,
wallet?.adapter,
mangoGroup.tokens[MNGO_INDEX].rootBank,
mngoNodeBank.publicKey,
mngoNodeBank.vault
@ -432,18 +433,25 @@ export default function Account() {
icon={<CurrencyDollarIcon />}
onClickButton={() => setShowAccountsModal(true)}
title={t('no-account-found')}
disabled={!wallet || !mangoGroup}
/>
)
) : (
<EmptyState
buttonText={t('connect')}
desc={t('connect-view')}
disabled={!wallet || !mangoGroup}
icon={<LinkIcon />}
onClickButton={() => wallet.connect()}
onClickButton={handleConnect}
title={t('connect-wallet')}
/>
)}
</div>
{!connected && (
<div className="mt-6 md:mt-3 md:rounded-lg md:bg-th-bkg-2 md:p-6">
<MangoAccountLookup />
</div>
)}
</PageBodyContainer>
{showAccountsModal ? (
<AccountsModal

218
pages/fees.tsx Normal file
View File

@ -0,0 +1,218 @@
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import TopBar from '../components/TopBar'
import PageBodyContainer from '../components/PageBodyContainer'
import useSrmAccount from '../hooks/useSrmAccount'
import {
MSRM_DECIMALS,
SRM_DECIMALS,
} from '@project-serum/serum/lib/token-instructions'
import { percentFormat } from '../utils/index'
import Tooltip from '../components/Tooltip'
import { InformationCircleIcon } from '@heroicons/react/outline'
import DepositMsrmModal from '../components/DepositMsrmModal'
import WithdrawMsrmModal from '../components/WithdrawMsrmModal'
import { useState } from 'react'
import { LinkButton } from '../components/Button'
import useMangoStore from '../stores/useMangoStore'
import { msrmMints, ZERO_BN } from '@blockworks-foundation/mango-client'
import useFees from '../hooks/useFees'
import { useWallet } from '@solana/wallet-adapter-react'
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props
},
}
}
export default function Fees() {
const { t } = useTranslation('common')
const { totalSrm, totalMsrm, rates } = useSrmAccount()
const { takerFee, makerFee } = useFees()
const { connected } = useWallet()
const [showDeposit, setShowDeposit] = useState(false)
const [showWithdraw, setShowWithdraw] = useState(false)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const walletTokens = useMangoStore((s) => s.wallet.tokens)
const cluster = useMangoStore.getState().connection.cluster
const ownerMsrmAccount = walletTokens.find((t) =>
t.account.mint.equals(msrmMints[cluster])
)
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />
<PageBodyContainer>
<div className="flex flex-col py-4 sm:flex-row md:pb-4 md:pt-10">
<h1>{t('fees')}</h1>
</div>
<div className="md:rounded-lg md:bg-th-bkg-2 md:p-6">
<h2 className="mb-4">{t('futures')}</h2>
<div className="grid grid-cols-1 grid-rows-2 pb-8 md:grid-cols-2 md:grid-rows-1 md:gap-4">
<div className="border-t border-th-bkg-4 p-3 sm:p-4 md:border-b">
<div className="pb-0.5 text-th-fgd-3">{t('maker-fee')}</div>
<div className="flex items-center">
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
{percentFormat.format(makerFee)}
</div>
</div>
<div className="flex items-center">
<p className="mb-0">
{t('if-referred', {
fee: percentFormat.format(
makerFee < 0
? makerFee + makerFee * 0.04
: makerFee - makerFee * 0.04
),
})}
</p>
<Tooltip content={t('if-referred-tooltip')}>
<div>
<InformationCircleIcon
className={`ml-1.5 h-5 w-5 cursor-help text-th-fgd-3`}
/>
</div>
</Tooltip>
</div>
</div>
<div className="border-b border-t border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-th-fgd-3">{t('taker-fee')}</div>
<div className="flex items-center">
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
{percentFormat.format(takerFee)}
</div>
</div>
<div className="flex items-center">
<p className="mb-0">
{t('if-referred', {
fee: percentFormat.format(
takerFee < 0
? takerFee + takerFee * 0.04
: takerFee - takerFee * 0.04
),
})}
</p>
<Tooltip content={t('if-referred-tooltip')}>
<div>
<InformationCircleIcon
className={`ml-1.5 h-5 w-5 cursor-help text-th-fgd-3`}
/>
</div>
</Tooltip>
</div>
</div>
</div>
<h2 className="mb-4">{t('serum-fees')}</h2>
<div className="grid grid-cols-1 grid-rows-2 pb-8 md:grid-cols-3 md:grid-rows-1 md:gap-4">
<div className="border-t border-th-bkg-4 p-3 sm:p-4 md:border-b">
<div className="pb-0.5 text-th-fgd-3">{t('maker-fee')}</div>
<div className="flex items-center">
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
{rates ? percentFormat.format(rates.maker) : null}
</div>
</div>
</div>
<div className="border-t border-th-bkg-4 p-3 sm:p-4 md:border-b">
<div className="flex items-center pb-0.5 text-th-fgd-3">
{t('taker-fee')}
<Tooltip
content={t('tooltip-serum-rebate', {
taker_percent: percentFormat.format(rates.taker),
})}
>
<div>
<InformationCircleIcon
className={`ml-1.5 h-5 w-5 cursor-help text-th-fgd-3`}
/>
</div>
</Tooltip>
</div>
<div className="flex items-center">
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
{rates
? new Intl.NumberFormat(undefined, {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 3,
}).format(rates.takerWithRebate)
: null}
</div>
</div>
</div>
<div className="border-b border-t border-th-bkg-4 p-3 sm:p-4">
<div className="flex items-center justify-between pb-0.5 text-th-fgd-3">
{totalMsrm > 0 ? 'MSRM' : 'SRM'} {t('deposits')}
{connected && mangoAccount ? (
<div className="flex justify-center space-x-3 pl-2">
<LinkButton
onClick={() => setShowDeposit(true)}
disabled={!ownerMsrmAccount}
>
{t('deposit')}
</LinkButton>
{mangoAccount.msrmAmount.gt(ZERO_BN) ? (
<LinkButton onClick={() => setShowWithdraw(true)}>
{t('withdraw')}
</LinkButton>
) : null}
</div>
) : null}
</div>
<div className="flex items-center">
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
{totalMsrm > 0
? totalMsrm.toLocaleString(undefined, {
maximumFractionDigits: MSRM_DECIMALS,
})
: totalSrm.toLocaleString(undefined, {
maximumFractionDigits: SRM_DECIMALS,
})}
</div>
</div>
</div>
</div>
<h2 className="mb-4">{t('other')}</h2>
<div className="grid grid-cols-1 grid-rows-3 pb-6 md:grid-cols-3 md:grid-rows-1 md:gap-4">
<div className="border-t border-th-bkg-4 p-3 sm:p-4 md:border-b">
<div className="pb-0.5 text-th-fgd-3">{t('withdraw')}</div>
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
0%
</div>
</div>
<div className="border-t border-th-bkg-4 p-3 sm:p-4 md:border-b">
<div className="pb-0.5 text-th-fgd-3">{t('borrow')}</div>
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
0%
</div>
</div>
<div className="border-b border-t border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-th-fgd-3">{t('lend')}</div>
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
0%
</div>
</div>
</div>
</div>
</PageBodyContainer>
{showDeposit ? (
<DepositMsrmModal
isOpen={showDeposit}
onClose={() => setShowDeposit(false)}
/>
) : null}
{showWithdraw ? (
<WithdrawMsrmModal
isOpen={showWithdraw}
onClose={() => setShowWithdraw(false)}
/>
) : null}
</div>
)
}

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react'
import React, { useEffect } from 'react'
import { useRouter } from 'next/router'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import useMangoStore, { serumProgramId } from '../stores/useMangoStore'
@ -19,10 +19,10 @@ import {
actionsSelector,
mangoAccountSelector,
marketConfigSelector,
walletConnectedSelector,
} from '../stores/selectors'
import { PublicKey } from '@solana/web3.js'
import FavoritesShortcutBar from '../components/FavoritesShortcutBar'
import { useWallet } from '@solana/wallet-adapter-react'
export async function getStaticProps({ locale }) {
return {
@ -37,12 +37,12 @@ export async function getStaticProps({ locale }) {
}
}
const PerpMarket = () => {
const PerpMarket: React.FC = () => {
const [alphaAccepted] = useLocalStorageState(ALPHA_MODAL_KEY, false)
const [showTour] = useLocalStorageState(SHOW_TOUR_KEY, false)
const { connected } = useWallet()
const groupConfig = useMangoGroupConfig()
const setMangoStore = useMangoStore((s) => s.set)
const connected = useMangoStore(walletConnectedSelector)
const mangoAccount = useMangoStore(mangoAccountSelector)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const marketConfig = useMangoStore(marketConfigSelector)
@ -62,12 +62,10 @@ const PerpMarket = () => {
unownedMangoAccountPubkey,
serumProgramId
)
console.log('unOwnedMangoAccount: ', unOwnedMangoAccount)
setMangoStore((state) => {
state.selectedMangoAccount.current = unOwnedMangoAccount
state.selectedMangoAccount.initialLoad = false
state.wallet.connected = true
})
actions.fetchTradeHistory()
actions.reloadOrders()

View File

@ -7,7 +7,6 @@ import {
mangoCacheSelector,
mangoGroupConfigSelector,
mangoGroupSelector,
walletSelector,
} from '../stores/selectors'
import Button, { IconButton } from '../components/Button'
import {
@ -47,6 +46,8 @@ import MobileTableHeader from '../components/mobile/MobileTableHeader'
import Input, { Label } from '../components/Input'
import InlineNotification from '../components/InlineNotification'
import useMangoAccount from '../hooks/useMangoAccount'
import { handleWalletConnect } from 'components/ConnectWalletButton'
import { useWallet } from '@solana/wallet-adapter-react'
export async function getStaticProps({ locale }) {
return {
@ -78,9 +79,8 @@ export default function Referral() {
const mangoCache = useMangoStore(mangoCacheSelector)
const { mangoAccount } = useMangoAccount()
const groupConfig = useMangoStore(mangoGroupConfigSelector)
const wallet = useMangoStore(walletSelector)
const connected = useMangoStore((s) => s.wallet.connected)
const actions = useMangoStore((s) => s.actions)
const { wallet, connected } = useWallet()
const referralHistory = useMangoStore((s) => s.referrals.history)
const referralTotalAmount = useMangoStore((s) => s.referrals.total)
@ -92,6 +92,8 @@ export default function Referral() {
>([])
const [hasCopied, setHasCopied] = useState(null)
const [showAccountsModal, setShowAccountsModal] = useState(false)
// const [hasReferrals] = useState(false) // Placeholder to show/hide users referral stats
const [loading, setLoading] = useState(false)
const [inputError, setInputError] = useState('')
const { width } = useViewport()
@ -149,6 +151,10 @@ export default function Referral() {
}
}
const handleConnect = useCallback(() => {
handleWalletConnect(wallet)
}, [wallet])
const submitRefLink = async () => {
let encodedRefLink: string
try {
@ -166,7 +172,7 @@ export default function Referral() {
const txid = await mangoClient.registerReferrerId(
mangoGroup,
mangoAccount,
wallet,
wallet?.adapter,
encodedRefLink
)
notify({
@ -517,6 +523,7 @@ export default function Referral() {
icon={<CurrencyDollarIcon />}
onClickButton={() => setShowAccountsModal(true)}
title={t('no-account-found')}
disabled={!wallet || !mangoGroup}
/>
</div>
</>
@ -529,8 +536,9 @@ export default function Referral() {
<div className="col-span-12 flex items-center justify-center rounded-md bg-th-bkg-3 p-6 lg:col-span-8">
<EmptyState
buttonText={t('connect')}
disabled={!wallet || !mangoGroup}
icon={<LinkIcon />}
onClickButton={() => wallet.connect()}
onClickButton={handleConnect}
title={t('connect-wallet')}
/>
</div>

View File

@ -29,6 +29,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useTranslation } from 'next-i18next'
import { useRouter } from 'next/router'
import { PublicKey } from '@solana/web3.js'
import { useWallet } from '@solana/wallet-adapter-react'
export async function getStaticProps({ locale }) {
return {
@ -98,9 +99,9 @@ export default function RiskCalculator() {
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const mangoConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const connected = useMangoStore((s) => s.wallet.connected)
const setMangoStore = useMangoStore((s) => s.set)
const router = useRouter()
const { connected } = useWallet()
const { pubkey } = router.query
// Set default state variables
@ -121,7 +122,7 @@ export default function RiskCalculator() {
router.push('/risk-calculator')
setScenarioInitialized(false)
}
}, [connected])
}, [connected, router])
useEffect(() => {
async function loadUnownedMangoAccount() {
@ -147,7 +148,7 @@ export default function RiskCalculator() {
if (pubkey) {
loadUnownedMangoAccount()
}
}, [pubkey, mangoGroup])
}, [pubkey, mangoGroup, router, setMangoStore])
useEffect(() => {
const handleRouteChange = () => {
@ -161,7 +162,7 @@ export default function RiskCalculator() {
return () => {
router.events.off('routeChangeStart', handleRouteChange)
}
}, [resetOnLeave])
}, [resetOnLeave, setMangoStore])
// Set rules for updating the scenario
useEffect(() => {

View File

@ -4,15 +4,11 @@ import useMangoStore from '../stores/useMangoStore'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import {
actionsSelector,
connectionSelector,
walletConnectedSelector,
walletSelector,
} from '../stores/selectors'
import { actionsSelector, connectionSelector } from '../stores/selectors'
import JupiterForm from '../components/JupiterForm'
import { zeroKey } from '@blockworks-foundation/mango-client'
import { useTranslation } from 'next-i18next'
import { useWallet } from '@solana/wallet-adapter-react'
export async function getStaticProps({ locale }) {
return {
@ -26,8 +22,7 @@ export async function getStaticProps({ locale }) {
export default function Swap() {
const { t } = useTranslation(['common', 'swap'])
const connection = useMangoStore(connectionSelector)
const connected = useMangoStore(walletConnectedSelector)
const wallet = useMangoStore(walletSelector)
const { connected, publicKey } = useWallet()
const actions = useMangoStore(actionsSelector)
useEffect(() => {
@ -39,9 +34,7 @@ export default function Swap() {
if (!connection) return null
const userPublicKey =
wallet?.publicKey && !zeroKey.equals(wallet.publicKey)
? wallet.publicKey
: null
publicKey && !zeroKey.equals(publicKey) ? publicKey : null
return (
<JupiterProvider

View File

@ -0,0 +1 @@
<svg fill="#ffffff" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 41.625 10.769531 C 37.644531 7.566406 31.347656 7.023438 31.078125 7.003906 C 30.660156 6.96875 30.261719 7.203125 30.089844 7.589844 C 30.074219 7.613281 29.9375 7.929688 29.785156 8.421875 C 32.417969 8.867188 35.652344 9.761719 38.578125 11.578125 C 39.046875 11.867188 39.191406 12.484375 38.902344 12.953125 C 38.710938 13.261719 38.386719 13.429688 38.050781 13.429688 C 37.871094 13.429688 37.6875 13.378906 37.523438 13.277344 C 32.492188 10.15625 26.210938 10 25 10 C 23.789063 10 17.503906 10.15625 12.476563 13.277344 C 12.007813 13.570313 11.390625 13.425781 11.101563 12.957031 C 10.808594 12.484375 10.953125 11.871094 11.421875 11.578125 C 14.347656 9.765625 17.582031 8.867188 20.214844 8.425781 C 20.0625 7.929688 19.925781 7.617188 19.914063 7.589844 C 19.738281 7.203125 19.34375 6.960938 18.921875 7.003906 C 18.652344 7.023438 12.355469 7.566406 8.320313 10.8125 C 6.214844 12.761719 2 24.152344 2 34 C 2 34.175781 2.046875 34.34375 2.132813 34.496094 C 5.039063 39.605469 12.972656 40.941406 14.78125 41 C 14.789063 41 14.800781 41 14.8125 41 C 15.132813 41 15.433594 40.847656 15.621094 40.589844 L 17.449219 38.074219 C 12.515625 36.800781 9.996094 34.636719 9.851563 34.507813 C 9.4375 34.144531 9.398438 33.511719 9.765625 33.097656 C 10.128906 32.683594 10.761719 32.644531 11.175781 33.007813 C 11.234375 33.0625 15.875 37 25 37 C 34.140625 37 38.78125 33.046875 38.828125 33.007813 C 39.242188 32.648438 39.871094 32.683594 40.238281 33.101563 C 40.601563 33.515625 40.5625 34.144531 40.148438 34.507813 C 40.003906 34.636719 37.484375 36.800781 32.550781 38.074219 L 34.378906 40.589844 C 34.566406 40.847656 34.867188 41 35.1875 41 C 35.199219 41 35.210938 41 35.21875 41 C 37.027344 40.941406 44.960938 39.605469 47.867188 34.496094 C 47.953125 34.34375 48 34.175781 48 34 C 48 24.152344 43.785156 12.761719 41.625 10.769531 Z M 18.5 30 C 16.566406 30 15 28.210938 15 26 C 15 23.789063 16.566406 22 18.5 22 C 20.433594 22 22 23.789063 22 26 C 22 28.210938 20.433594 30 18.5 30 Z M 31.5 30 C 29.566406 30 28 28.210938 28 26 C 28 23.789063 29.566406 22 31.5 22 C 33.433594 22 35 23.789063 35 26 C 35 28.210938 33.433594 30 31.5 30 Z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,56 @@
<svg width="2963" height="864" viewBox="0 0 2963 864" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1274.13 488.608C1277.75 495.431 1281.05 502.575 1284.04 510.039C1287.24 517.289 1290.33 524.646 1293.31 532.11C1296.29 524.433 1299.39 516.863 1302.58 509.399C1305.78 501.935 1309.19 494.792 1312.81 487.968L1449.66 231.754C1451.37 228.556 1453.07 225.997 1454.78 224.077C1456.69 222.158 1458.72 220.772 1460.85 219.919C1463.2 219.066 1465.75 218.533 1468.52 218.32C1471.3 218.107 1474.6 218 1478.44 218H1543.34V680.528H1467.57V381.772C1467.57 376.228 1467.67 370.15 1467.89 363.54C1468.31 356.929 1468.84 350.212 1469.48 343.388L1329.76 605.679C1326.56 611.65 1322.41 616.341 1317.29 619.753C1312.17 622.952 1306.21 624.551 1299.39 624.551H1287.56C1280.73 624.551 1274.77 622.952 1269.65 619.753C1264.53 616.341 1260.38 611.65 1257.18 605.679L1115.54 342.428C1116.39 349.465 1116.92 356.396 1117.14 363.22C1117.56 369.83 1117.78 376.014 1117.78 381.772V680.528H1042V218H1106.91C1110.74 218 1114.05 218.107 1116.82 218.32C1119.59 218.533 1122.04 219.066 1124.17 219.919C1126.52 220.772 1128.65 222.158 1130.57 224.077C1132.48 225.997 1134.3 228.556 1136 231.754L1274.13 488.608Z" fill="white"/>
<path d="M1889.09 680.528H1853.6C1846.14 680.528 1840.28 679.462 1836.02 677.329C1831.76 674.984 1828.56 670.399 1826.43 663.575L1819.39 640.225C1811.08 647.688 1802.87 654.299 1794.77 660.056C1786.89 665.601 1778.68 670.292 1770.15 674.131C1761.63 677.969 1752.57 680.848 1742.98 682.767C1733.38 684.686 1722.73 685.646 1711 685.646C1697.15 685.646 1684.36 683.833 1672.63 680.208C1660.91 676.37 1650.79 670.719 1642.26 663.255C1633.95 655.792 1627.45 646.515 1622.76 635.427C1618.07 624.338 1615.72 611.437 1615.72 596.723C1615.72 584.355 1618.92 572.2 1625.31 560.258C1631.92 548.103 1642.79 537.227 1657.93 527.631C1673.06 517.822 1693.2 509.719 1718.36 503.322C1743.51 496.924 1774.74 493.299 1812.04 492.446V473.254C1812.04 451.29 1807.35 435.083 1797.97 424.634C1788.8 413.972 1775.38 408.641 1757.68 408.641C1744.89 408.641 1734.24 410.134 1725.71 413.119C1717.18 416.104 1709.72 419.516 1703.33 423.355C1697.15 426.98 1691.39 430.285 1686.06 433.271C1680.73 436.256 1674.87 437.749 1668.48 437.749C1663.15 437.749 1658.57 436.363 1654.73 433.59C1650.89 430.818 1647.8 427.406 1645.46 423.355L1631.07 398.085C1668.8 363.54 1714.31 346.267 1767.6 346.267C1786.78 346.267 1803.83 349.465 1818.75 355.863C1833.89 362.047 1846.68 370.79 1857.12 382.092C1867.57 393.181 1875.45 406.508 1880.78 422.075C1886.32 437.642 1889.09 454.702 1889.09 473.254V680.528ZM1735.62 631.268C1743.72 631.268 1751.18 630.522 1758 629.029C1764.82 627.537 1771.22 625.298 1777.19 622.312C1783.37 619.327 1789.23 615.702 1794.77 611.437C1800.53 606.958 1806.28 601.734 1812.04 595.763V540.426C1789.02 541.492 1769.73 543.518 1754.17 546.504C1738.82 549.276 1726.46 552.901 1717.08 557.379C1707.7 561.857 1700.98 567.082 1696.93 573.053C1693.1 579.023 1691.18 585.527 1691.18 592.564C1691.18 606.425 1695.23 616.341 1703.33 622.312C1711.64 628.283 1722.41 631.268 1735.62 631.268Z" fill="white"/>
<path d="M1966.75 680.528V352.344H2015.03C2025.26 352.344 2031.98 357.142 2035.17 366.738L2040.61 392.648C2047.22 385.824 2054.14 379.64 2061.39 374.095C2068.85 368.551 2076.63 363.753 2084.73 359.701C2093.04 355.65 2101.89 352.557 2111.27 350.425C2120.65 348.293 2130.88 347.226 2141.96 347.226C2159.87 347.226 2175.75 350.318 2189.6 356.503C2203.46 362.473 2214.97 371.003 2224.14 382.092C2233.51 392.967 2240.55 406.082 2245.24 421.436C2250.14 436.576 2252.59 453.316 2252.59 471.655V680.528H2173.62V471.655C2173.62 451.61 2168.93 436.149 2159.55 425.274C2150.38 414.185 2136.53 408.641 2117.98 408.641C2104.34 408.641 2091.55 411.733 2079.62 417.917C2067.68 424.101 2056.38 432.524 2045.72 443.187V680.528H1966.75Z" fill="white"/>
<path d="M2443.7 346.587C2457.77 346.587 2470.99 348.079 2483.35 351.065C2495.71 353.837 2507.01 357.995 2517.24 363.54H2611.56V392.967C2611.56 397.872 2610.28 401.71 2607.73 404.483C2605.17 407.255 2600.8 409.174 2594.62 410.24L2565.2 415.678C2567.33 421.222 2568.93 427.087 2570 433.271C2571.28 439.455 2571.92 445.959 2571.92 452.783C2571.92 468.989 2568.61 483.703 2562 496.924C2555.61 509.932 2546.66 521.021 2535.15 530.19C2523.85 539.36 2510.31 546.504 2494.54 551.621C2478.98 556.526 2462.03 558.978 2443.7 558.978C2431.34 558.978 2419.3 557.806 2407.57 555.46C2397.34 561.644 2392.23 568.574 2392.23 576.251C2392.23 582.862 2395.21 587.766 2401.18 590.965C2407.36 593.951 2415.35 596.083 2425.16 597.362C2435.18 598.642 2446.47 599.495 2459.05 599.921C2471.63 600.135 2484.52 600.774 2497.74 601.841C2510.95 602.907 2523.85 604.826 2536.43 607.598C2549 610.157 2560.19 614.315 2570 620.073C2580.02 625.831 2588.01 633.721 2593.98 643.743C2600.16 653.552 2603.25 666.241 2603.25 681.807C2603.25 696.308 2599.63 710.382 2592.38 724.03C2585.34 737.678 2575.01 749.833 2561.36 760.495C2547.94 771.157 2531.42 779.687 2511.81 786.084C2492.2 792.695 2469.81 796 2444.66 796C2419.72 796 2398.09 793.548 2379.76 788.643C2361.42 783.952 2346.18 777.554 2334.03 769.451C2322.1 761.561 2313.14 752.391 2307.18 741.942C2301.21 731.493 2298.22 720.618 2298.22 709.316C2298.22 693.962 2302.91 681.061 2312.29 670.612C2321.67 660.163 2334.67 651.847 2351.3 645.662C2343.2 641.184 2336.7 635.213 2331.8 627.75C2326.89 620.286 2324.44 610.584 2324.44 598.642C2324.44 593.737 2325.29 588.726 2327 583.608C2328.71 578.277 2331.26 573.053 2334.67 567.935C2338.3 562.817 2342.77 558.019 2348.1 553.541C2353.43 548.849 2359.72 544.691 2366.97 541.066C2350.34 532.11 2337.23 520.168 2327.64 505.241C2318.26 490.314 2313.57 472.828 2313.57 452.783C2313.57 436.576 2316.77 421.969 2323.16 408.961C2329.77 395.74 2338.83 384.544 2350.34 375.375C2362.06 365.992 2375.81 358.848 2391.59 353.944C2407.57 349.039 2424.94 346.587 2443.7 346.587ZM2530.35 695.242C2530.35 688.845 2528.43 683.62 2524.6 679.568C2520.76 675.517 2515.54 672.425 2508.93 670.292C2502.32 667.947 2494.54 666.241 2485.59 665.174C2476.85 664.108 2467.47 663.362 2457.45 662.935C2447.65 662.296 2437.41 661.762 2426.76 661.336C2416.31 660.696 2406.19 659.737 2396.38 658.457C2387.43 663.362 2380.18 669.226 2374.64 676.05C2369.31 682.66 2366.65 690.337 2366.65 699.08C2366.65 704.838 2368.03 710.169 2370.8 715.074C2373.79 720.191 2378.37 724.563 2384.55 728.188C2390.95 731.813 2399.15 734.586 2409.17 736.505C2419.19 738.637 2431.45 739.703 2445.94 739.703C2460.65 739.703 2473.33 738.531 2483.99 736.185C2494.65 734.052 2503.39 730.96 2510.21 726.909C2517.24 723.07 2522.36 718.379 2525.55 712.834C2528.75 707.503 2530.35 701.639 2530.35 695.242ZM2443.7 508.439C2453.51 508.439 2462.03 507.16 2469.28 504.601C2476.53 501.829 2482.5 498.097 2487.19 493.406C2492.09 488.714 2495.71 483.063 2498.06 476.453C2500.62 469.842 2501.89 462.592 2501.89 454.702C2501.89 438.495 2496.99 425.7 2487.19 416.318C2477.59 406.722 2463.1 401.924 2443.7 401.924C2424.31 401.924 2409.7 406.722 2399.9 416.318C2390.31 425.7 2385.51 438.495 2385.51 454.702C2385.51 462.379 2386.68 469.522 2389.03 476.133C2391.59 482.743 2395.21 488.501 2399.9 493.406C2404.8 498.097 2410.88 501.829 2418.12 504.601C2425.58 507.16 2434.11 508.439 2443.7 508.439Z" fill="white"/>
<path d="M2801.21 347.226C2825.73 347.226 2847.9 351.171 2867.72 359.061C2887.76 366.952 2904.81 378.147 2918.88 392.648C2932.95 407.148 2943.82 424.847 2951.49 445.745C2959.16 466.643 2963 489.994 2963 515.796C2963 541.812 2959.16 565.269 2951.49 586.167C2943.82 607.065 2932.95 624.871 2918.88 639.585C2904.81 654.299 2887.76 665.601 2867.72 673.491C2847.9 681.381 2825.73 685.326 2801.21 685.326C2776.7 685.326 2754.43 681.381 2734.39 673.491C2714.35 665.601 2697.19 654.299 2682.91 639.585C2668.85 624.871 2657.87 607.065 2649.98 586.167C2642.31 565.269 2638.47 541.812 2638.47 515.796C2638.47 489.994 2642.31 466.643 2649.98 445.745C2657.87 424.847 2668.85 407.148 2682.91 392.648C2697.19 378.147 2714.35 366.952 2734.39 359.061C2754.43 351.171 2776.7 347.226 2801.21 347.226ZM2801.21 624.551C2828.5 624.551 2848.64 615.382 2861.64 597.043C2874.86 578.704 2881.47 551.835 2881.47 516.436C2881.47 481.037 2874.86 454.062 2861.64 435.51C2848.64 416.957 2828.5 407.681 2801.21 407.681C2773.5 407.681 2753.04 417.064 2739.83 435.83C2726.61 454.382 2720 481.251 2720 516.436C2720 551.621 2726.61 578.49 2739.83 597.043C2753.04 615.382 2773.5 624.551 2801.21 624.551Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M337.746 201.289C337.751 201.283 337.755 201.277 337.76 201.27C374.05 219.89 413.76 226.27 451.76 226.16C473.84 246.44 492.21 270.26 509.47 294.72C516.627 304.937 523.082 315.628 528.79 326.72C541.989 352.136 551.696 379.028 561.518 406.237C564.398 414.217 567.289 422.225 570.28 430.23C571.217 433.656 572.196 437.084 573.217 440.514L573.27 440.5C588.44 492.53 610.59 548.11 640.27 594L640.241 594.012C650.058 609.087 661.055 623.361 673.13 636.7C675.506 639.27 677.936 641.816 680.369 644.365L680.37 644.366L680.371 644.367L680.374 644.37C691.742 656.28 703.19 668.274 709.68 683.28C717.76 701.98 717.14 723.49 712.28 743.28C689.35 836.56 599.52 861.07 513.67 863.33L513.711 863.223C481.522 863.864 449.556 861.465 421.21 858.56C421.21 858.56 284.5 844.41 168.62 759.69L164.88 756.91C164.88 756.91 164.88 756.91 164.881 756.909C151.355 746.83 138.451 735.941 126.24 724.3C93.76 693.3 64.86 658.14 42.76 619.54C42.9078 619.392 43.0553 619.243 43.2026 619.095C40.587 614.388 38.0795 609.634 35.68 604.83C14.3 562.04 1.27 515.46 0.679999 465.95C-0.325631 382.873 28.0953 297.795 82.1176 236.113C82.0984 236.062 82.0792 236.011 82.06 235.96C111.31 203.92 147.87 178.75 191.15 164.42C218.283 155.354 246.768 151.001 275.37 151.55C292.775 171.987 313.954 188.874 337.746 201.289ZM271.153 744.85C290.711 737.711 309.24 728.11 326.518 716.279C309.131 728.03 290.575 737.637 271.153 744.85Z" fill="url(#paint0_linear)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M621.498 133.628C621.512 133.628 621.526 133.628 621.54 133.627L622.72 132.867C470.3 -127.133 271 77.5671 271 77.5671L271.285 78.0685C271.273 78.0714 271.262 78.0742 271.25 78.0771C385.856 279.01 603.664 145.087 621.498 133.628Z" fill="url(#paint1_linear)"/>
<path d="M432.56 581.44C390.56 681.84 309.92 748.32 212.22 758.39C210.12 758.67 183.38 760.78 168.62 759.69C284.5 844.41 421.21 858.56 421.21 858.56C450.48 861.56 483.61 864.02 516.86 863.15C528.57 832.58 535.16 797.58 533.27 757.87C528.88 665.64 582.36 618.29 640.27 594C610.59 548.11 588.44 492.53 573.27 440.5C528.05 452.53 470.62 490.36 432.56 581.44Z" fill="url(#paint2_linear)"/>
<path d="M531.44 757.22C533.34 796.93 525.38 832.76 513.67 863.33C599.52 861.07 689.35 836.56 712.28 743.28C717.14 723.49 717.76 701.98 709.68 683.28C701.8 665.06 686.61 651.28 673.13 636.7C660.519 622.769 649.084 607.818 638.94 592C581.08 616.3 527.05 665 531.44 757.22Z" fill="url(#paint3_linear)"/>
<path d="M570.28 430.23C557.09 394.93 545.86 359.59 528.79 326.72C523.082 315.628 516.627 304.937 509.47 294.72C492.21 270.26 473.84 246.44 451.76 226.16C413.76 226.27 374.05 219.89 337.76 201.27C301 253.41 258.94 341.86 297.44 442.82C354.29 591.92 238.92 693.82 164.88 756.91L168.62 759.69C182.502 760.773 196.455 760.573 210.3 759.09C307.99 749.01 393.3 680.93 435.3 580.54C473.37 489.46 528.75 454.86 573.91 442.82C572.637 438.62 571.427 434.423 570.28 430.23Z" fill="url(#paint4_linear)"/>
<path d="M86.09 231.67C29.49 293.67 -0.350001 380.86 0.679999 465.95C1.27 515.46 14.3 562.04 35.68 604.83C38.887 611.25 42.287 617.583 45.88 623.83C164.87 504.39 121.88 326.42 86.09 231.67Z" fill="url(#paint5_linear)"/>
<path d="M299.44 442.82C260.94 341.82 302.06 253.95 338.77 201.82C314.561 189.357 293.024 172.28 275.37 151.55C246.768 151.001 218.283 155.354 191.15 164.42C147.87 178.75 111.31 203.92 82.06 235.96C117.06 328.63 159.12 502.72 42.76 619.54C64.86 658.14 93.76 693.3 126.24 724.3C139.051 736.513 152.625 747.899 166.88 758.39C240.92 695.33 356.29 591.92 299.44 442.82Z" fill="url(#paint6_linear)"/>
<path d="M443 94.13C523.57 125.92 580 134.53 620.91 133.3L622.09 132.54C469.67 -127.46 270.37 77.24 270.37 77.24L270.66 77.75C313.65 70.13 376.13 67.76 443 94.13Z" fill="url(#paint7_linear)"/>
<path d="M444 92.33C377.17 66 314.33 67 270.62 77.75C385.23 278.69 603.05 144.75 620.87 133.3C579.93 134.53 524.57 124.12 444 92.33Z" fill="url(#paint8_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="-88.5" y1="273.5" x2="843.5" y2="832" gradientUnits="userSpaceOnUse">
<stop stop-color="#E54033"/>
<stop offset="0.489583" stop-color="#FECA1A"/>
<stop offset="1" stop-color="#AFD803"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="263632" y1="31154.5" x2="205286" y2="-28862.6" gradientUnits="userSpaceOnUse">
<stop offset="0.15" stop-color="#6CBF00"/>
<stop offset="1" stop-color="#AFD803"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="72.43" y1="766.73" x2="656.43" y2="624.73" gradientUnits="userSpaceOnUse">
<stop offset="0.21" stop-color="#E54033"/>
<stop offset="0.84" stop-color="#FECA1A"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="532.54" y1="727.34" x2="712.74" y2="728.69" gradientUnits="userSpaceOnUse">
<stop stop-color="#FECA1A"/>
<stop offset="0.4" stop-color="#FECA1A"/>
<stop offset="1" stop-color="#AFD803"/>
</linearGradient>
<linearGradient id="paint4_linear" x1="124.65" y1="770.37" x2="494.1" y2="270.2" gradientUnits="userSpaceOnUse">
<stop offset="0.16" stop-color="#E54033"/>
<stop offset="0.84" stop-color="#FECA1A"/>
</linearGradient>
<linearGradient id="paint5_linear" x1="70.85" y1="273.39" x2="54.49" y2="596.45" gradientUnits="userSpaceOnUse">
<stop stop-color="#FECA1A"/>
<stop offset="0.76" stop-color="#E54033"/>
</linearGradient>
<linearGradient id="paint6_linear" x1="251.58" y1="189.5" x2="152.91" y2="564.17" gradientUnits="userSpaceOnUse">
<stop offset="0.16" stop-color="#FECA1A"/>
<stop offset="1" stop-color="#E54033"/>
</linearGradient>
<linearGradient id="paint7_linear" x1="289.8" y1="10.1199" x2="655.13" y2="144.78" gradientUnits="userSpaceOnUse">
<stop offset="0.15" stop-color="#6CBF00"/>
<stop offset="1" stop-color="#AFD803"/>
</linearGradient>
<linearGradient id="paint8_linear" x1="263631" y1="31154.2" x2="205285" y2="-28862.9" gradientUnits="userSpaceOnUse">
<stop offset="0.15" stop-color="#6CBF00"/>
<stop offset="1" stop-color="#AFD803"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,3 +1,5 @@
{
"about-to-withdraw": "You're about to withdraw",
"above": "Above",
@ -136,11 +138,17 @@
"export-data-empty": "No data to export",
"export-data-success": "CSV exported successfully",
"export-pnl-csv": "Export PnL CSV",
"export-trades-csv": "Export Trades CSV",
"favorite": "Favorite",
"favorites": "Favorites",
"fee": "Fee",
"fees": "Fees",
"fee-discount": "Fee Discount",
"filter": "Filter",
"filters-selected": "{{selectedFilters}} selected",
"filter-trade-history": "Filter Trade History",
"first-deposit-desc": "There is a one-time cost of 0.035 SOL when you make your first deposit. This covers the rent on the Solana Blockchain for your account.",
"from": "From",
"funding": "Funding",
"funding-chart-title": "Funding Last 30 days (current bar is delayed)",
"futures": "Futures",
@ -156,6 +164,8 @@
"hourly-borrow-interest": "Hourly Borrow Interest",
"hourly-deposit-interest": "Hourly Deposit Interest",
"hourly-funding": "Hourly Funding",
"if-referred": "{{fee}} if referred or 10k MNGO",
"if-referred-tooltip": "If you create your Mango Account from a referral link or have 10k MNGO in your Mango Account you get a 0.04% discount off futures fees.",
"in-orders": "In Orders",
"include-perp": "Include Perp",
"include-spot": "Include Spot",
@ -182,6 +192,7 @@
"layout-tip-title": "Customize Layout",
"learn": "Documentation",
"learn-more": "Learn more",
"lend": "Lend",
"lets-go": "Let's Go",
"leverage": "Leverage",
"leverage-too-high": "Leverage too high. Reduce the amount to withdraw",
@ -249,6 +260,7 @@
"no-markets": "No markets found",
"no-orders": "No open orders",
"no-perp": "No perp positions",
"no-trades-found": "No trades found...",
"no-unsettled": "There are no unsettled funds",
"no-wallet": "No wallet address",
"node-url": "RPC Node URL",
@ -263,6 +275,7 @@
"orderbook": "Orderbook",
"orderbook-animation": "Orderbook Animation",
"orders": "Orders",
"other": "Other",
"performance": "Performance",
"performance-insights": "Performance Insights",
"period-progress": "Period Progress",
@ -300,6 +313,7 @@
"redeem-all": "Redeem All",
"redeem-failure": "Error redeeming MNGO",
"redeem-pnl": "Redeem",
"redeem-positive": "Redeem positive",
"redeem-success": "Successfully redeemed MNGO",
"referrals": "Referrals",
"refresh": "Refresh",
@ -310,6 +324,7 @@
"repay-partial": "Repay {{percentage}}% of borrow",
"reposition": "Drag to reposition",
"reset": "Reset",
"reset-filters": "Reset Filters",
"rolling-change": "24hr Change",
"rpc-endpoint": "RPC Endpoint",
"save": "Save",
@ -356,6 +371,7 @@
"themes-tip-title": "Color Themes",
"time": "Time",
"timeframe-desc": "Last {{timeframe}}",
"to": "To",
"token": "Token",
"too-large": "Size Too Large",
"tooltip-account-liquidated": "Account will be liquidated if Health Ratio reaches 0% and will continue until Init Health is above 0.",
@ -406,6 +422,7 @@
"unsettled-balance": "Redeemable Value",
"unsettled-balances": "Unsettled Balances",
"unsettled-positions": "Unsettled Positions",
"update-filters": "Update Filters",
"use-explorer-one": "Use the ",
"use-explorer-three": "to verify any delayed transactions.",
"use-explorer-two": "Explorer ",

View File

@ -6,5 +6,4 @@
"public-key": "Delegate Public Key",
"delegate-updated": "Delegate Updated",
"set-error": "Could not set Delegate",
"invalid-key": "Invalid public key"
}

View File

@ -1,3 +1,5 @@
{
"about-to-withdraw": "Estas a punto de retirar",
"above": "Encima",
@ -135,11 +137,17 @@
"export-data-empty": "No hay datos para exportar",
"export-data-success": "CSV exportado con éxito",
"export-pnl-csv": "Export PnL CSV",
"export-trades-csv": "Export Trades CSV",
"favorite": "Favorito",
"favorites": "Favoritos",
"fee": "Tarifa",
"fees": "Fees",
"fee-discount": "Comisiones",
"filter": "Filter",
"filters-selected": "{{selectedFilters}} selected",
"filter-trade-history": "Filter Trade History",
"first-deposit-desc": "Necesita 0.035 SOL para crear una cuenta de mango.",
"from": "From",
"funding": "Fondos",
"funding-chart-title": "Fondos (últimos 30 días)",
"futures": "Futuros",
@ -155,6 +163,8 @@
"hourly-borrow-interest": "Interés por préstamo por hora",
"hourly-deposit-interest": "Interés por depósito por hora",
"hourly-funding": "Financiamiento por hora",
"if-referred": "{{fee}} if referred or 10k MNGO",
"if-referred-tooltip": "If you create your Mango Account from a referral link or have 10k MNGO in your Mango Account you get a 0.04% discount off futures fees.",
"in-orders": "En órdenes",
"includes-borrow": "Incluye el préstamo",
"init-error": "No se pudo realizar la operación de depósito y cuenta de margen inicial",
@ -179,6 +189,7 @@
"layout-tip-title": "Personalizar diseño",
"learn": "Aprender",
"learn-more": "Aprender más",
"lend": "Lend",
"lets-go": "Vamos",
"leverage": "Apalancamiento",
"leverage-too-high": "Apalancamiento demasiado alto. Reducir la cantidad a retirar",
@ -246,6 +257,7 @@
"no-markets": "No se encontraron mercados",
"no-orders": "No hay órdenes abiertas",
"no-perp": "No hay puestos de delincuentes",
"no-trades-found": "No trades found...",
"no-unsettled": "No hay fondos pendientes",
"no-wallet": "Sin dirección de billetera",
"node-url": "URL del nodo RPC",
@ -260,6 +272,7 @@
"orderbook": "Libro de órdenes",
"orderbook-animation": "Animación del libro de ordenes",
"orders": "Órdenes",
"other": "Other",
"performance": "Performance",
"performance-insights": "Performance Insights",
"period-progress": "Period Progress",
@ -307,6 +320,7 @@
"repay-partial": "Pagar el {{porcentaje}}% del préstamo",
"reposition": "Arrastra para reposicionar",
"reset": "Reset",
"reset-filters": "Reset Filters",
"rolling-change": "24hr Change",
"rpc-endpoint": "Punto final de RPC",
"save": "Ahorrar",
@ -353,6 +367,7 @@
"themes-tip-title": "Temas de color",
"time": "Tiempo",
"timeframe-desc": "Last {{timeframe}}",
"to": "To",
"token": "Simbólico",
"too-large": "Tamaño demasiado grande",
"tooltip-account-liquidated": "La cuenta se liquidará si la relación de salud alcanza el 0% y continuará hasta que la salud inicial sea superior a 0.",
@ -403,6 +418,7 @@
"unsettled-balance": "Saldo pendiente",
"unsettled-balances": "Saldos pendientes",
"unsettled-positions": "Posiciones sin saldar",
"update-filters": "Update Filters",
"use-explorer-one": "Usar el ",
"use-explorer-three": "para verificar cualquier transacción retrasada.",
"use-explorer-two": "Explorer ",

View File

@ -5,6 +5,5 @@
"info": "Grant control to another Solana account to use Mango on your behalf.",
"public-key": "Delegate Public Key",
"delegate-updated": "Delegate Updated",
"set-error": "Could not set Delegate",
"invalid-key": "Invalid public key"
"set-error": "Could not set Delegate"
}

View File

@ -135,11 +135,17 @@
"export-data-empty": "无资料可导出",
"export-data-success": "CSV导出成功",
"export-pnl-csv": "导出盈亏CSV",
"export-trades-csv": "Export Trades CSV",
"favorite": "喜爱",
"favorites": "喜爱",
"fee": "费率",
"fees": "Fees",
"fee-discount": "费率折扣",
"filter": "Filter",
"filters-selected": "{{selectedFilters}} selected",
"filter-trade-history": "Filter Trade History",
"first-deposit-desc": "创建Mango帐户最少需要0.035 SOL。",
"from": "From",
"funding": "资金费",
"funding-chart-title": "资金费 前30天(图表有点延迟)",
"futures": "永续合约",
@ -155,6 +161,8 @@
"hourly-borrow-interest": "1小时借贷利息",
"hourly-deposit-interest": "1小时存款利息",
"hourly-funding": "1小时资金费",
"if-referred": "{{fee}} if referred or 10k MNGO",
"if-referred-tooltip": "If you create your Mango Account from a referral link or have 10k MNGO in your Mango Account you get a 0.04% discount off futures fees.",
"in-orders": "在掛单中",
"includes-borrow": "包括存入",
"init-error": "创建Mango帐户与存款出错了",
@ -179,6 +187,7 @@
"layout-tip-title": "个人化页面布局",
"learn": "学习",
"learn-more": "学习",
"lend": "Lend",
"lets-go": "前往",
"leverage": "杠杆",
"leverage-too-high": "杠杆太高。请减少取款数量",
@ -246,6 +255,7 @@
"no-markets": "无市场",
"no-orders": "您没有订单",
"no-perp": "您没有永续合约持仓",
"no-trades-found": "No trades found...",
"no-unsettled": "您没有未结清金额",
"no-wallet": "没有钱包地址",
"node-url": "RPC终点URL",
@ -260,6 +270,7 @@
"orderbook": "订单簿",
"orderbook-animation": "订单动画",
"orders": "订单",
"other": "Other",
"performance": "表现",
"performance-insights": "表现分析",
"period-progress": "期间进度",
@ -307,6 +318,7 @@
"repay-partial": "归还{{percentage}}%借贷",
"reposition": "推动以重新定位",
"reset": "重置",
"reset-filters": "Reset Filters",
"rolling-change": "24小时变动",
"rpc-endpoint": "RPC终点",
"save": "保存",
@ -353,6 +365,7 @@
"themes-tip-title": "颜色模式",
"time": "时间",
"timeframe-desc": "前{{timeframe}}",
"to": "To",
"token": "币种",
"too-large": "数量太大",
"tooltip-account-liquidated": "若帐户健康度降到0%您的帐户会被清算直到初始健康度达到0以上了。",
@ -403,6 +416,7 @@
"unsettled-balance": "未实现盈亏",
"unsettled-balances": "未结清余额",
"unsettled-positions": "未结清持仓",
"update-filters": "Update Filters",
"use-explorer-one": "使用",
"use-explorer-three": "来验证延迟的交易",
"use-explorer-two": "浏览器",

View File

@ -5,6 +5,5 @@
"info": "将此帐户委托其他Solana帐户控制。",
"public-key": "受托钱包地址",
"delegate-updated": "已更换受托钱包",
"set-error": "设置委托钱包出错",
"invalid-key": "您输入的地址有问题"
"set-error": "设置委托钱包出错"
}

View File

@ -135,11 +135,17 @@
"export-data-empty": "無資料可導出",
"export-data-success": "CSV導出成功",
"export-pnl-csv": "導出盈虧CSV",
"export-trades-csv": "Export Trades CSV",
"favorite": "喜愛",
"favorites": "喜愛",
"fee": "費率",
"fees": "Fees",
"fee-discount": "費率折扣",
"filter": "Filter",
"filters-selected": "{{selectedFilters}} selected",
"filter-trade-history": "Filter Trade History",
"first-deposit-desc": "創建Mango帳戶最少需要0.035 SOL。",
"from": "From",
"funding": "資金費",
"funding-chart-title": "資金費 前30天(圖表有點延遲)",
"futures": "永續合約",
@ -155,6 +161,8 @@
"hourly-borrow-interest": "1小時借貸利息",
"hourly-deposit-interest": "1小時存款利息",
"hourly-funding": "1小時資金費",
"if-referred": "{{fee}} if referred or 10k MNGO",
"if-referred-tooltip": "If you create your Mango Account from a referral link or have 10k MNGO in your Mango Account you get a 0.04% discount off futures fees.",
"in-orders": "在掛單中",
"includes-borrow": "包括存入",
"init-error": "創建Mango帳戶與存款出錯了",
@ -179,6 +187,7 @@
"layout-tip-title": "個人化頁面佈局",
"learn": "學習",
"learn-more": "學習",
"lend": "Lend",
"lets-go": "前往",
"leverage": "槓桿",
"leverage-too-high": "槓桿太高。請減少取款數量",
@ -246,6 +255,7 @@
"no-markets": "無市場",
"no-orders": "您沒有訂單",
"no-perp": "您沒有永續合約持倉",
"no-trades-found": "No trades found...",
"no-unsettled": "您沒有未結清金額",
"no-wallet": "沒有錢包地址",
"node-url": "RPC終點URL",
@ -260,6 +270,7 @@
"orderbook": "掛單簿",
"orderbook-animation": "訂單動畫",
"orders": "訂單",
"other": "Other",
"performance": "表現",
"performance-insights": "表現分析",
"period-progress": "期間進度",
@ -307,6 +318,7 @@
"repay-partial": "歸還{{percentage}}%借貸",
"reposition": "推動以重新定位",
"reset": "重置",
"reset-filters": "Reset Filters",
"rolling-change": "24小時變動",
"rpc-endpoint": "RPC終點",
"save": "保存",
@ -353,6 +365,7 @@
"themes-tip-title": "顏色模式",
"time": "時間",
"timeframe-desc": "前{{timeframe}}",
"to": "To",
"token": "幣種",
"too-large": "數量太大",
"tooltip-account-liquidated": "若帳戶健康度降到0%您的帳戶會被清算直到初始健康度達到0以上了。",
@ -403,6 +416,7 @@
"unsettled-balance": "未實現盈虧",
"unsettled-balances": "未結清餘額",
"unsettled-positions": "未結清持倉",
"update-filters": "Update Filters",
"use-explorer-one": "使用",
"use-explorer-three": "來驗證延遲的交易",
"use-explorer-two": "瀏覽器",

View File

@ -5,6 +5,5 @@
"info": "將此帳戶委託其他Solana帳戶控制。",
"public-key": "受託錢包地址",
"delegate-updated": "已更換受託錢包",
"set-error": "設置委託錢包出錯",
"invalid-key": "您輸入的地址有問題"
"set-error": "設置委託錢包出錯"
}

View File

@ -43,8 +43,3 @@ export const setStoreSelector = (state: MangoStore) => state.set
export const accountInfosSelector = (state: MangoStore) => state.accountInfos
export const tradeHistorySelector = (state: MangoStore) => state.tradeHistory
export const walletSelector = (state: MangoStore) => state.wallet.current
export const walletConnectedSelector = (state: MangoStore) =>
state.wallet.connected

View File

@ -26,7 +26,7 @@ import {
BlockhashTimes,
} from '@blockworks-foundation/mango-client'
import { AccountInfo, Commitment, Connection, PublicKey } from '@solana/web3.js'
import { EndpointInfo, WalletAdapter } from '../@types/types'
import { EndpointInfo } from '../@types/types'
import { isDefined, zipDict } from '../utils'
import { Notification, notify } from '../utils/notifications'
import { LAST_ACCOUNT_KEY } from '../components/AccountsModal'
@ -39,6 +39,7 @@ import { MSRM_DECIMALS } from '@project-serum/serum/lib/token-instructions'
import { getProfilePicture, ProfilePicture } from '@solflare-wallet/pfp'
import { decodeBook } from '../hooks/useHydrateStore'
import { IOrderLineAdapter } from '../public/charting_library/charting_library'
import { Wallet } from '@solana/wallet-adapter-react'
export const ENDPOINTS: EndpointInfo[] = [
{
@ -191,9 +192,6 @@ export type MangoStore = {
triggerCondition: 'above' | 'below'
}
wallet: {
providerUrl: string
connected: boolean
current: WalletAdapter | undefined
tokens: WalletToken[] | any[]
pfp: ProfilePicture | undefined
}
@ -201,13 +199,14 @@ export type MangoStore = {
uiLocked: boolean
}
tradeHistory: {
initialLoad: boolean
spot: any[]
perp: any[]
parsed: any[]
}
set: (x: (x: MangoStore) => void) => void
actions: {
fetchAllMangoAccounts: () => Promise<void>
fetchAllMangoAccounts: (wallet: Wallet) => Promise<void>
fetchMangoGroup: () => Promise<void>
[key: string]: (args?) => void
}
@ -323,9 +322,6 @@ const useMangoStore = create<
triggerCondition: 'above',
},
wallet: {
providerUrl: '',
connected: false,
current: undefined,
tokens: [],
pfp: undefined,
},
@ -341,6 +337,7 @@ const useMangoStore = create<
success: '',
},
tradeHistory: {
initialLoad: false,
spot: [],
perp: [],
parsed: [],
@ -350,19 +347,18 @@ const useMangoStore = create<
},
set: (fn) => set(produce(fn)),
actions: {
async fetchWalletTokens() {
async fetchWalletTokens(wallet: Wallet) {
const groupConfig = get().selectedMangoGroup.config
const wallet = get().wallet.current
const connected = get().wallet.connected
const connected = wallet?.adapter?.connected
const connection = get().connection.current
const cluster = get().connection.cluster
const set = get().set
if (wallet?.publicKey && connected) {
if (wallet?.adapter?.publicKey && connected) {
const ownedTokenAccounts =
await getTokenAccountsByOwnerWithWrappedSol(
connection,
wallet.publicKey
wallet.adapter.publicKey
)
const tokens = []
ownedTokenAccounts.forEach((account) => {
@ -393,10 +389,9 @@ const useMangoStore = create<
})
}
},
async fetchProfilePicture() {
async fetchProfilePicture(wallet: Wallet) {
const set = get().set
const wallet = get().wallet.current
const walletPk = wallet?.publicKey
const walletPk = wallet?.adapter?.publicKey
const connection = get().connection.current
if (!walletPk) return
@ -411,30 +406,29 @@ const useMangoStore = create<
console.log('Could not get profile picture', e)
}
},
async fetchAllMangoAccounts() {
async fetchAllMangoAccounts(wallet) {
const set = get().set
const mangoGroup = get().selectedMangoGroup.current
const mangoClient = get().connection.client
const wallet = get().wallet.current
const actions = get().actions
if (!wallet?.adapter?.publicKey || !mangoGroup) return
const delegateFilter = [
{
memcmp: {
offset: MangoAccountLayout.offsetOf('delegate'),
bytes: wallet?.publicKey.toBase58(),
bytes: wallet.adapter.publicKey?.toBase58(),
},
},
]
const accountSorter = (a, b) =>
a.publicKey.toBase58() > b.publicKey.toBase58() ? 1 : -1
if (!wallet?.publicKey || !mangoGroup) return
return Promise.all([
mangoClient.getMangoAccountsForOwner(
mangoGroup,
wallet?.publicKey,
wallet.adapter.publicKey,
true
),
mangoClient.getAllMangoAccounts(mangoGroup, delegateFilter, false),
@ -472,7 +466,7 @@ const useMangoStore = create<
})
.catch((err) => {
if (mangoAccountRetryAttempt < 2) {
actions.fetchAllMangoAccounts()
actions.fetchAllMangoAccounts(wallet)
mangoAccountRetryAttempt++
} else {
notify({
@ -662,6 +656,9 @@ const useMangoStore = create<
console.error('Error fetching trade history', e)
})
}
set((state) => {
state.tradeHistory.initialLoad = true
})
},
async reloadMangoAccount() {
const set = get().set

45
styles/datepicker.css Normal file
View File

@ -0,0 +1,45 @@
/* Date Picker */
.react-datepicker {
@apply rounded-md border-0 bg-th-bkg-3 font-body text-th-fgd-2 ring-1 ring-th-fgd-4;
}
.react-datepicker-popper[data-placement^='bottom'] {
@apply pt-1;
}
.react-datepicker__header {
@apply rounded-tl-md border-none bg-th-bkg-1 text-th-fgd-1;
}
.react-datepicker__header:not(.react-datepicker__header--has-time-select) {
@apply rounded-tr-md;
}
.react-datepicker__current-month {
@apply text-th-fgd-1;
}
.react-datepicker__day-name {
@apply text-th-fgd-2;
}
.react-datepicker__triangle {
@apply hidden;
}
.react-datepicker__day {
@apply text-th-fgd-2 hover:bg-th-bkg-4;
}
.react-datepicker__day--selected {
@apply bg-th-primary text-th-bkg-1 hover:bg-th-primary hover:text-th-bkg-1;
}
.react-datepicker__day--keyboard-selected {
@apply bg-transparent ring-2 ring-inset ring-th-fgd-4;
}
.react-datepicker__day--today {
@apply rounded-md ring-2 ring-inset ring-th-fgd-4;
}

View File

@ -13,7 +13,8 @@
"resolveJsonModule": true,
"jsx": "preserve",
"isolatedModules": true,
"incremental": true
"incremental": true,
"baseUrl": "."
},
"exclude": ["node_modules", ".next", "out", "public/datafeeds"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"]

View File

@ -1,4 +1,5 @@
import { MangoAccount, TokenAccount } from '@blockworks-foundation/mango-client'
import { Wallet } from '@solana/wallet-adapter-react'
import { PublicKey } from '@solana/web3.js'
import useMangoStore from '../stores/useMangoStore'
@ -7,24 +8,24 @@ export async function deposit({
fromTokenAcc,
mangoAccount,
accountName,
wallet,
}: {
amount: number
fromTokenAcc: TokenAccount
mangoAccount?: MangoAccount
accountName?: string
wallet: Wallet
}) {
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const wallet = useMangoStore.getState().wallet.current
const tokenIndex = mangoGroup.getTokenIndex(fromTokenAcc.mint)
const mangoClient = useMangoStore.getState().connection.client
const referrer = useMangoStore.getState().referrerPk
console.log('referrerPk', referrer)
if (mangoAccount) {
return await mangoClient.deposit(
mangoGroup,
mangoAccount,
wallet,
wallet?.adapter,
mangoGroup.tokens[tokenIndex].rootBank,
mangoGroup.rootBankAccounts[tokenIndex].nodeBankAccounts[0].publicKey,
mangoGroup.rootBankAccounts[tokenIndex].nodeBankAccounts[0].vault,
@ -34,13 +35,13 @@ export async function deposit({
} else {
const existingAccounts = await mangoClient.getMangoAccountsForOwner(
mangoGroup,
wallet.publicKey,
wallet?.adapter?.publicKey,
false
)
console.log('in deposit and create, referrer is', referrer)
return await mangoClient.createMangoAccountAndDeposit(
mangoGroup,
wallet,
wallet?.adapter,
mangoGroup.tokens[tokenIndex].rootBank,
mangoGroup.rootBankAccounts[tokenIndex].nodeBankAccounts[0].publicKey,
mangoGroup.rootBankAccounts[tokenIndex].nodeBankAccounts[0].vault,
@ -57,21 +58,22 @@ export async function withdraw({
amount,
token,
allowBorrow,
wallet,
}: {
amount: number
token: PublicKey
allowBorrow: boolean
wallet: Wallet
}) {
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const wallet = useMangoStore.getState().wallet.current
const tokenIndex = mangoGroup.getTokenIndex(token)
const mangoClient = useMangoStore.getState().connection.client
return await mangoClient.withdraw(
mangoGroup,
mangoAccount,
wallet,
wallet?.adapter,
mangoGroup.tokens[tokenIndex].rootBank,
mangoGroup.rootBankAccounts[tokenIndex].nodeBankAccounts[0].publicKey,
mangoGroup.rootBankAccounts[tokenIndex].nodeBankAccounts[0].vault,

View File

@ -1,116 +0,0 @@
import EventEmitter from 'eventemitter3'
import { PublicKey, Transaction } from '@solana/web3.js'
import { notify } from '../../utils/notifications'
import { WalletAdapter } from '../../@types/types'
interface BitpieWallet {
getAccount(): Promise<string>
signTransaction(transaction: Transaction): Promise<Transaction>
signAllTransactions(transactions: Transaction[]): Promise<Transaction[]>
}
interface BitpieWalletWindow extends Window {
bitpie?: BitpieWallet
}
declare const window: BitpieWalletWindow
export class BitpieWalletAdapter extends EventEmitter implements WalletAdapter {
private _connecting: boolean
private _wallet: BitpieWallet | null
private _publicKey: PublicKey | null
constructor() {
super()
this._connecting = false
this._wallet = null
this._publicKey = null
}
get publicKey(): PublicKey | null {
return this._publicKey
}
get ready(): boolean {
return typeof window !== 'undefined' && !!window.bitpie
}
get connecting(): boolean {
return this._connecting
}
get connected(): boolean {
return !!this._wallet
}
get autoApprove() {
return true
}
async connect(): Promise<void> {
try {
if (this.connected || this.connecting) return
this._connecting = true
const wallet = typeof window !== 'undefined' && window.bitpie
if (!wallet) return
let account: string
try {
account = await wallet.getAccount()
} catch (error: any) {
notify({
title: 'Connection Error',
type: 'error',
description:
'Please install Bitpie wallet and then reload this page.',
})
}
this._wallet = wallet
this._publicKey = new PublicKey(account)
this.emit('connect')
} catch (error: any) {
this.emit('error', error)
throw error
} finally {
this._connecting = false
}
}
async disconnect(): Promise<void> {
if (this._wallet) {
this._wallet = null
this._publicKey = null
}
this.emit('disconnect')
}
async signTransaction(transaction: Transaction): Promise<Transaction> {
try {
const wallet = this._wallet
if (!wallet) return
return (await wallet.signTransaction(transaction)) || transaction
} catch (error: any) {
this.emit('error', error)
throw error
}
}
async signAllTransactions(
transactions: Transaction[]
): Promise<Transaction[]> {
try {
const wallet = this._wallet
if (!wallet) return
return (await wallet.signAllTransactions(transactions)) || transactions
} catch (error: any) {
this.emit('error', error)
throw error
}
}
}

View File

@ -1,101 +0,0 @@
import EventEmitter from 'eventemitter3'
import { PublicKey, Transaction } from '@solana/web3.js'
import { notify } from '../../utils/notifications'
import { DEFAULT_PUBLIC_KEY, WalletAdapter } from '../../@types/types'
type GlowEvent = 'disconnect' | 'connect'
type GlowRequestMethod =
| 'connect'
| 'disconnect'
| 'signTransaction'
| 'signAllTransactions'
interface GlowProvider {
publicKey?: PublicKey
isConnected?: boolean
autoApprove?: boolean
signTransaction: (transaction: Transaction) => Promise<Transaction>
signAllTransactions: (transactions: Transaction[]) => Promise<Transaction[]>
connect: () => Promise<void>
disconnect: () => Promise<void>
on: (event: GlowEvent, handler: (args: any) => void) => void
request: (method: GlowRequestMethod, params: any) => Promise<any>
listeners: (event: GlowEvent) => (() => void)[]
}
export class GlowWalletAdapter extends EventEmitter implements WalletAdapter {
constructor() {
super()
this.connect = this.connect.bind(this)
}
private get _provider(): GlowProvider | undefined {
if ((window as any)?.glowSolana?.isGlow) {
return (window as any).glowSolana
}
return undefined
}
private _handleConnect = (...args) => {
this.emit('connect', ...args)
}
private _handleDisconnect = (...args) => {
this.emit('disconnect', ...args)
}
get connected() {
return this._provider?.isConnected || false
}
get autoApprove() {
return this._provider?.autoApprove || false
}
async signAllTransactions(
transactions: Transaction[]
): Promise<Transaction[]> {
if (!this._provider) {
return transactions
}
return this._provider.signAllTransactions(transactions)
}
get publicKey() {
return this._provider?.publicKey || DEFAULT_PUBLIC_KEY
}
async signTransaction(transaction: Transaction) {
if (!this._provider) {
return transaction
}
return this._provider.signTransaction(transaction)
}
connect() {
if (!this._provider) {
window.open('https://glow.app/', '_blank')
notify({
title: 'Connection Error',
type: 'error',
description: 'Please install Glow wallet and then reload this page.',
})
return
}
if (!this._provider.listeners('connect').length) {
this._provider?.on('connect', this._handleConnect)
}
if (!this._provider.listeners('disconnect').length) {
this._provider?.on('disconnect', this._handleDisconnect)
}
return this._provider?.connect()
}
disconnect() {
if (this._provider) {
this._provider.disconnect()
}
}
}

View File

@ -1,94 +0,0 @@
import EventEmitter from 'eventemitter3'
import { PublicKey, Transaction } from '@solana/web3.js'
import { DEFAULT_PUBLIC_KEY, WalletAdapter } from '../../@types/types'
type HuobiEvent = 'disconnect' | 'connect'
type HuobiRequestMethod =
| 'connect'
| 'disconnect'
| 'signTransaction'
| 'signAllTransactions'
interface HuobiProvider {
publicKey?: PublicKey
isConnected?: boolean
autoApprove?: boolean
signTransaction: (transaction: Transaction) => Promise<Transaction>
signAllTransactions: (transactions: Transaction[]) => Promise<Transaction[]>
connect: () => Promise<void>
disconnect: () => Promise<void>
on: (event: HuobiEvent, handler: (args: any) => void) => void
request: (method: HuobiRequestMethod, params: any) => Promise<any>
listeners: (event: HuobiEvent) => (() => void)[]
}
export class HuobiWalletAdapter extends EventEmitter implements WalletAdapter {
constructor() {
super()
this.connect = this.connect.bind(this)
}
private get _provider(): HuobiProvider | undefined {
if ((window as any)?.huobiWallet?.isHuobiWallet) {
return (window as any).huobiWallet
}
return undefined
}
private _handleConnect = (...args) => {
this.emit('connect', ...args)
}
private _handleDisconnect = (...args) => {
this.emit('disconnect', ...args)
}
get connected() {
return this._provider?.isConnected || false
}
get autoApprove() {
return this._provider?.autoApprove || false
}
async signAllTransactions(
transactions: Transaction[]
): Promise<Transaction[]> {
if (!this._provider) {
return transactions
}
return this._provider.signAllTransactions(transactions)
}
get publicKey() {
return this._provider?.publicKey || DEFAULT_PUBLIC_KEY
}
async signTransaction(transaction: Transaction) {
if (!this._provider) {
return transaction
}
return this._provider.signTransaction(transaction)
}
connect() {
if (!this._provider) {
return
}
if (!this._provider.listeners('connect').length) {
this._provider?.on('connect', this._handleConnect)
}
if (!this._provider.listeners('disconnect').length) {
this._provider?.on('disconnect', this._handleDisconnect)
}
return this._provider?.connect()
}
disconnect() {
if (this._provider) {
this._provider.disconnect()
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,104 +0,0 @@
import EventEmitter from 'eventemitter3'
import { PublicKey, Transaction } from '@solana/web3.js'
import { notify } from '../../utils/notifications'
import { DEFAULT_PUBLIC_KEY, WalletAdapter } from '../../@types/types'
type PhantomEvent = 'disconnect' | 'connect'
type PhantomRequestMethod =
| 'connect'
| 'disconnect'
| 'signTransaction'
| 'signAllTransactions'
interface PhantomProvider {
publicKey?: PublicKey
isConnected?: boolean
autoApprove?: boolean
signTransaction: (transaction: Transaction) => Promise<Transaction>
signAllTransactions: (transactions: Transaction[]) => Promise<Transaction[]>
connect: () => Promise<void>
disconnect: () => Promise<void>
on: (event: PhantomEvent, handler: (args: any) => void) => void
request: (method: PhantomRequestMethod, params: any) => Promise<any>
listeners: (event: PhantomEvent) => (() => void)[]
}
export class PhantomWalletAdapter
extends EventEmitter
implements WalletAdapter
{
constructor() {
super()
this.connect = this.connect.bind(this)
}
private get _provider(): PhantomProvider | undefined {
if ((window as any)?.solana?.isPhantom) {
return (window as any).solana
}
return undefined
}
private _handleConnect = (...args) => {
this.emit('connect', ...args)
}
private _handleDisconnect = (...args) => {
this.emit('disconnect', ...args)
}
get connected() {
return this._provider?.isConnected || false
}
get autoApprove() {
return this._provider?.autoApprove || false
}
async signAllTransactions(
transactions: Transaction[]
): Promise<Transaction[]> {
if (!this._provider) {
return transactions
}
return this._provider.signAllTransactions(transactions)
}
get publicKey() {
return this._provider?.publicKey || DEFAULT_PUBLIC_KEY
}
async signTransaction(transaction: Transaction) {
if (!this._provider) {
return transaction
}
return this._provider.signTransaction(transaction)
}
connect() {
if (!this._provider) {
window.open('https://phantom.app/', '_blank')
notify({
title: 'Connection Error',
type: 'error',
description: 'Please install Phantom wallet and then reload this page.',
})
return
}
if (!this._provider.listeners('connect').length) {
this._provider?.on('connect', this._handleConnect)
}
if (!this._provider.listeners('disconnect').length) {
this._provider?.on('disconnect', this._handleDisconnect)
}
return this._provider?.connect()
}
disconnect() {
if (this._provider) {
this._provider.disconnect()
}
}
}

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