deposit modal
This commit is contained in:
parent
b5562c161c
commit
71db71b347
|
@ -1,38 +1,64 @@
|
|||
import xw from 'xwind'
|
||||
import { Listbox, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
|
||||
import { abbreviateAddress, getSymbolForTokenMintAddress } from '../utils'
|
||||
import useMarketList from '../hooks/useMarketList'
|
||||
// import useMarket from '../hooks/useMarket'
|
||||
import { nativeToUi } from '@blockworks-foundation/mango-client/lib/utils'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { Account } from '@solana/web3.js'
|
||||
import { tokenPrecision } from '../utils/index'
|
||||
|
||||
interface MarketSelectProps {
|
||||
accounts?: any[]
|
||||
selectedAccount?: Account
|
||||
onSelectAccount: () => void
|
||||
}
|
||||
|
||||
const AccountSelect = ({accounts, selectedAccount, onSelectAccount}): MarketSelectProps => {
|
||||
const AccountSelect = ({ accounts, selectedAccount, onSelectAccount }) => {
|
||||
const { getTokenIndex } = useMarketList()
|
||||
const mintDecimals = useMangoStore((s) => s.selectedMangoGroup.mintDecimals)
|
||||
const handleChange = (value) => {
|
||||
onSelectAccount(value)
|
||||
const newAccount = accounts.find((a) => a.publicKey.toString() === value)
|
||||
onSelectAccount(newAccount)
|
||||
}
|
||||
|
||||
const getBalanceForAccount = (account) => {
|
||||
const mintAddress = account?.account.mint.toString()
|
||||
const balance = nativeToUi(
|
||||
account?.account?.amount,
|
||||
mintDecimals[getTokenIndex(mintAddress)]
|
||||
)
|
||||
const symbol = getSymbolForTokenMintAddress(mintAddress)
|
||||
|
||||
return balance.toFixed(tokenPrecision[symbol])
|
||||
}
|
||||
|
||||
return (
|
||||
<div css={xw`ml-4 relative inline-block -mb-1`}>
|
||||
<Listbox value={selectedAccount} onChange={handleChange}>
|
||||
<div css={xw`relative inline-block w-full`}>
|
||||
<Listbox
|
||||
value={selectedAccount?.publicKey.toString()}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Button
|
||||
css={xw`border border-mango-dark-lighter focus:outline-none focus:ring-1 focus:ring-mango-yellow p-2 w-56`}
|
||||
css={xw`border border-mango-dark-lighter focus:outline-none focus:ring-1 focus:ring-mango-yellow p-2 w-full`}
|
||||
>
|
||||
<div
|
||||
css={xw`flex items-center text-lg justify-between font-light`}
|
||||
css={xw`flex items-center text-base justify-between font-light`}
|
||||
>
|
||||
{selectedAccount}
|
||||
<div css={xw`flex items-center flex-grow`}>
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${getSymbolForTokenMintAddress(
|
||||
selectedAccount?.account?.mint.toString()
|
||||
).toLowerCase()}.svg`}
|
||||
css={xw`mr-4`}
|
||||
/>
|
||||
{abbreviateAddress(selectedAccount?.publicKey)}
|
||||
<div css={xw`ml-4 text-sm text-right flex-grow`}>
|
||||
({getBalanceForAccount(selectedAccount)})
|
||||
</div>
|
||||
</div>
|
||||
{open ? (
|
||||
<ChevronUpIcon css={xw`h-5 w-5 mr-1`} />
|
||||
<ChevronUpIcon css={xw`h-5 w-5 ml-2`} />
|
||||
) : (
|
||||
<ChevronDownIcon css={xw`h-5 w-5 mr-1`} />
|
||||
<ChevronDownIcon css={xw`h-5 w-5 ml-2`} />
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Button>
|
||||
|
@ -47,24 +73,49 @@ const AccountSelect = ({accounts, selectedAccount, onSelectAccount}): MarketSele
|
|||
>
|
||||
<Listbox.Options
|
||||
static
|
||||
css={xw`z-20 p-1 absolute left-0 w-56 mt-1 bg-mango-dark-light origin-top-left divide-y divide-mango-dark-lighter shadow-lg outline-none`}
|
||||
css={xw`z-20 p-1 absolute left-0 w-full mt-1 bg-mango-dark-light origin-top-left divide-y divide-mango-dark-lighter shadow-lg outline-none border border-mango-dark-lighter`}
|
||||
>
|
||||
<div css={xw`opacity-50 p-2`}>Markets</div>
|
||||
{Object.entries(accounts).map((account) => (
|
||||
<Listbox.Option key={account} value={account}>
|
||||
{({ selected }) => (
|
||||
<div
|
||||
css={[
|
||||
xw`p-2 text-base hover:bg-mango-dark-lighter hover:cursor-pointer tracking-wider font-light`,
|
||||
selected &&
|
||||
xw`text-mango-yellow bg-mango-dark-lighter`,
|
||||
]}
|
||||
>
|
||||
{account}
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
<div css={xw`opacity-50 p-2`}>
|
||||
Your Connected Wallet Token Accounts
|
||||
</div>
|
||||
{accounts.map((account) => {
|
||||
const symbolForAccount = getSymbolForTokenMintAddress(
|
||||
account?.account?.mint.toString()
|
||||
)
|
||||
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={account?.publicKey.toString()}
|
||||
value={account?.publicKey.toString()}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div
|
||||
css={[
|
||||
xw`p-2 text-sm hover:bg-mango-dark-lighter hover:cursor-pointer tracking-wider font-light`,
|
||||
selected &&
|
||||
xw`text-mango-yellow bg-mango-dark-lighter`,
|
||||
]}
|
||||
>
|
||||
<div css={xw`flex items-center space-x-2`}>
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${symbolForAccount.toLowerCase()}.svg`}
|
||||
/>
|
||||
<div css={xw`flex-grow text-left`}>
|
||||
{abbreviateAddress(account?.publicKey)}
|
||||
</div>
|
||||
<div css={xw`text-sm`}>
|
||||
{getBalanceForAccount(account)} (
|
||||
{symbolForAccount})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
import React, { useMemo, useState } from 'react'
|
||||
import xw from 'xwind'
|
||||
import { nativeToUi } from '@blockworks-foundation/mango-client/lib/utils'
|
||||
import Modal from './Modal'
|
||||
import { Button } from './styles'
|
||||
import AccountSelect from './AccountSelect'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import useMarketList from '../hooks/useMarketList'
|
||||
import { getSymbolForTokenMintAddress, tokenPrecision } from '../utils/index'
|
||||
import useConnection from '../hooks/useConnection'
|
||||
import { deposit, initMarginAccountAndDeposit } from '../utils/mango'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import Loading from './Loading'
|
||||
|
||||
const DepositModal = ({ isOpen, onClose }) => {
|
||||
const [inputAmount, setInputAmount] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const { symbols } = useMarketList()
|
||||
const { getTokenIndex } = useMarketList()
|
||||
const { connection, programId } = useConnection()
|
||||
const mintDecimals = useMangoStore((s) => s.selectedMangoGroup.mintDecimals)
|
||||
const walletAccounts = useMangoStore((s) => s.wallet.balances)
|
||||
const actions = useMangoStore((s) => s.actions)
|
||||
const depositAccounts = useMemo(
|
||||
() =>
|
||||
walletAccounts.filter((acc) =>
|
||||
Object.values(symbols).includes(acc.account.mint.toString())
|
||||
),
|
||||
[symbols, walletAccounts]
|
||||
)
|
||||
const [selectedAccount, setSelectedAccount] = useState(depositAccounts[0])
|
||||
|
||||
// TODO: remove duplication in AccountSelect
|
||||
const getBalanceForAccount = (account) => {
|
||||
const mintAddress = account?.account.mint.toString()
|
||||
const balance = nativeToUi(
|
||||
account?.account?.amount,
|
||||
mintDecimals[getTokenIndex(mintAddress)]
|
||||
)
|
||||
const symbol = getSymbolForTokenMintAddress(mintAddress)
|
||||
|
||||
return balance.toFixed(tokenPrecision[symbol])
|
||||
}
|
||||
|
||||
const setMaxForSelectedAccount = () => {
|
||||
const max = getBalanceForAccount(selectedAccount)
|
||||
setInputAmount(max)
|
||||
}
|
||||
|
||||
const handleDeposit = () => {
|
||||
setSubmitting(true)
|
||||
const marginAccount = useMangoStore.getState().selectedMarginAccount.current
|
||||
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
|
||||
const wallet = useMangoStore.getState().wallet.current
|
||||
if (!marginAccount && mangoGroup) {
|
||||
initMarginAccountAndDeposit(
|
||||
connection,
|
||||
new PublicKey(programId),
|
||||
mangoGroup,
|
||||
wallet,
|
||||
selectedAccount.account.mint,
|
||||
selectedAccount.publicKey,
|
||||
Number(inputAmount)
|
||||
)
|
||||
.then((response: Array<any>) => {
|
||||
actions.fetchWalletBalances()
|
||||
setSubmitting(false)
|
||||
console.log('success', response)
|
||||
onClose()
|
||||
})
|
||||
.catch((err) => {
|
||||
setSubmitting(false)
|
||||
console.error(err)
|
||||
alert('error depositing')
|
||||
onClose()
|
||||
})
|
||||
} else {
|
||||
deposit(
|
||||
connection,
|
||||
programId,
|
||||
mangoGroup,
|
||||
marginAccount,
|
||||
wallet,
|
||||
selectedAccount.account.mint,
|
||||
selectedAccount.publicKey,
|
||||
Number(inputAmount)
|
||||
)
|
||||
.then((response: string) => {
|
||||
actions.fetchWalletBalances()
|
||||
setSubmitting(false)
|
||||
console.log('success', response)
|
||||
onClose()
|
||||
})
|
||||
.catch((err) => {
|
||||
setSubmitting(false)
|
||||
console.error(err)
|
||||
alert('error depositing')
|
||||
onClose()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<Modal.Header>
|
||||
<div css={xw`text-mango-med-light flex-shrink invisible`}>X</div>
|
||||
<div
|
||||
css={xw`text-mango-med-light flex-grow text-center flex items-center justify-center`}
|
||||
>
|
||||
<div css={xw`flex-initial`}>Select: </div>
|
||||
<div css={xw`ml-4 flex-grow`}>
|
||||
<AccountSelect
|
||||
accounts={depositAccounts}
|
||||
selectedAccount={selectedAccount}
|
||||
onSelectAccount={setSelectedAccount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div css={xw`text-mango-med-light flex-shrink ml-6 mr-2 text-lg`}>
|
||||
<button onClick={onClose} css={xw`hover:text-mango-yellow`}>
|
||||
<svg
|
||||
viewBox="64 64 896 896"
|
||||
focusable="false"
|
||||
data-icon="close"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<div css={xw`pb-6 px-8`}>
|
||||
<div css={xw`mt-3 text-center sm:mt-5`}>
|
||||
<div css={xw`mt-6 bg-mango-dark-light rounded-md flex items-center`}>
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${getSymbolForTokenMintAddress(
|
||||
selectedAccount?.account?.mint.toString()
|
||||
).toLowerCase()}.svg`}
|
||||
css={xw`ml-3`}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
css={xw`outline-none bg-mango-dark-light w-full py-4 mx-3 text-2xl text-gray-300 flex-grow`}
|
||||
placeholder="0.00"
|
||||
value={inputAmount}
|
||||
onChange={(e) => setInputAmount(e.target.value)}
|
||||
></input>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={setMaxForSelectedAccount}
|
||||
css={xw`m-2 rounded py-1`}
|
||||
>
|
||||
Max
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div css={xw`mt-5 sm:mt-6 flex justify-center`}>
|
||||
<Button type="button" onClick={handleDeposit}>
|
||||
<div css={xw`flex items-center`}>
|
||||
{submitting && <Loading />}
|
||||
Deposit
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DepositModal)
|
|
@ -0,0 +1,28 @@
|
|||
import xw from 'xwind'
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<svg
|
||||
css={xw`animate-spin -ml-1 mr-3 h-5 w-5`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
css={xw`opacity-25`}
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
css={xw`opacity-75`}
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
|
@ -1,23 +1,18 @@
|
|||
import { Popover } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import xw from 'xwind'
|
||||
// import { nativeToUi } from '@blockworks-foundation/mango-client/lib/utils'
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
// import useConnection from '../hooks/useConnection'
|
||||
// import useMarginAccount from '../hooks/useMarginAcccount'
|
||||
import FloatingElement from './FloatingElement'
|
||||
import { Button, ElementTitle } from './styles'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import useMarketList from '../hooks/useMarketList'
|
||||
import { tokenPrecision } from '../utils/index'
|
||||
import Modal from './Modal'
|
||||
import DepositModal from './DepositModal'
|
||||
|
||||
export default function MarginStats() {
|
||||
// const { connection } = useConnection()
|
||||
// const { marginAccount, mangoGroup } = useMarginAccount()
|
||||
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const selectedMarginAccount = useMangoStore(
|
||||
(s) => s.selectedMarginAccount.current
|
||||
|
@ -27,13 +22,13 @@ export default function MarginStats() {
|
|||
const [showDepositModal, setShowDepositModal] = useState(false)
|
||||
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
|
||||
|
||||
const handleDeposit = () => {
|
||||
setShowDepositModal(true)
|
||||
}
|
||||
const handleCloseDeposit = useCallback(() => {
|
||||
setShowDepositModal(false)
|
||||
}, [])
|
||||
|
||||
const handleWithdraw = () => {
|
||||
setShowWithdrawModal(true)
|
||||
}
|
||||
const handleCloseWithdraw = useCallback(() => {
|
||||
setShowWithdrawModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -50,9 +45,11 @@ export default function MarginStats() {
|
|||
placement="topLeft"
|
||||
trigger="hover"
|
||||
>
|
||||
<InformationCircleIcon
|
||||
css={xw`h-5 w-5 ml-2 text-mango-yellow cursor-help`}
|
||||
/>
|
||||
<div>
|
||||
<InformationCircleIcon
|
||||
css={xw`h-5 w-5 ml-2 text-mango-yellow cursor-help`}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</ElementTitle>
|
||||
{selectedMangoGroup ? (
|
||||
|
@ -120,12 +117,12 @@ export default function MarginStats() {
|
|||
) : null}
|
||||
<div css={xw`flex justify-around items-center mt-4`}>
|
||||
<div>
|
||||
<Button onClick={handleDeposit}>
|
||||
<Button onClick={() => setShowDepositModal(true)}>
|
||||
<span>Deposit</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={handleWithdraw} css={xw`ml-4`}>
|
||||
<Button onClick={() => setShowWithdrawModal(true)} css={xw`ml-4`}>
|
||||
<span>Withdraw</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -135,15 +132,12 @@ export default function MarginStats() {
|
|||
</div>
|
||||
</FloatingElement>
|
||||
{showDepositModal && (
|
||||
<Modal
|
||||
isOpen={showDepositModal}
|
||||
onClose={() => setShowDepositModal(false)}
|
||||
/>
|
||||
<DepositModal isOpen={showDepositModal} onClose={handleCloseDeposit} />
|
||||
)}
|
||||
{showWithdrawModal && (
|
||||
<Modal
|
||||
<DepositModal
|
||||
isOpen={showWithdrawModal}
|
||||
onClose={() => setShowWithdrawModal(false)}
|
||||
onClose={handleCloseWithdraw}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
// import { useState } from 'react'
|
||||
import xw from 'xwind'
|
||||
import { Portal } from 'react-portal'
|
||||
import { Button } from './styles'
|
||||
|
||||
const Modal = ({ isOpen, onClose }) => {
|
||||
const handleClick = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const Modal = ({ isOpen, onClose, children }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<div
|
||||
|
@ -21,7 +15,7 @@ const Modal = ({ isOpen, onClose }) => {
|
|||
>
|
||||
{isOpen ? (
|
||||
<div
|
||||
css={xw`fixed inset-0 bg-black bg-opacity-40 transition-opacity`}
|
||||
css={xw`fixed inset-0 bg-black bg-opacity-20 transition-opacity`}
|
||||
aria-hidden="true"
|
||||
onClick={onClose}
|
||||
></div>
|
||||
|
@ -36,28 +30,9 @@ const Modal = ({ isOpen, onClose }) => {
|
|||
|
||||
{isOpen ? (
|
||||
<div
|
||||
css={xw`inline-block align-bottom bg-mango-dark rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full`}
|
||||
css={xw`inline-block align-bottom bg-mango-dark border border-mango-dark-light rounded-lg text-left overflow-hidden shadow-lg transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full`}
|
||||
>
|
||||
<div css={xw`pb-6 px-8`}>
|
||||
<div css={xw`mt-3 text-center sm:mt-5`}>
|
||||
<div css={xw`mt-6 bg-mango-dark-light rounded-md`}>
|
||||
<label htmlFor=""></label>
|
||||
<input
|
||||
type="text"
|
||||
css={xw`outline-none bg-mango-dark-light w-full py-4 mx-3 text-2xl text-gray-300`}
|
||||
placeholder="0.00"
|
||||
></input>
|
||||
</div>
|
||||
</div>
|
||||
<div css={xw`mt-5 sm:mt-6 flex justify-center space-x-4`}>
|
||||
<Button type="button" onClick={handleClick}>
|
||||
Max
|
||||
</Button>
|
||||
<Button type="button" onClick={handleClick}>
|
||||
Deposit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -68,7 +43,7 @@ const Modal = ({ isOpen, onClose }) => {
|
|||
|
||||
const Header = ({ children }) => {
|
||||
return (
|
||||
<div css={xw`flex items-center bg-mango-dark-light py-4 px-8`}>
|
||||
<div css={xw`flex items-center bg-mango-dark-light py-4 px-4`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -40,6 +40,7 @@ const useMarginAccount = () => {
|
|||
setMangoStore((state) => {
|
||||
state.selectedMangoGroup.current = mangoGroup
|
||||
state.selectedMangoGroup.srmAccount = srmAccountInfo
|
||||
state.selectedMangoGroup.mintDecimals = mangoGroup.mintDecimals
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo } from 'react'
|
||||
import { useMemo, useCallback } from 'react'
|
||||
import useConnection from './useConnection'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
|
@ -13,7 +13,7 @@ const useMarketList = () => {
|
|||
[cluster, mangoGroupName]
|
||||
)
|
||||
|
||||
const symbols = useMemo(
|
||||
const symbolsForMangoGroup = useMemo(
|
||||
() => IDS[cluster]?.mango_groups[mangoGroupName]?.symbols || {},
|
||||
[cluster, mangoGroupName]
|
||||
)
|
||||
|
@ -31,11 +31,18 @@ const useMarketList = () => {
|
|||
[spotMarkets, dexProgramId]
|
||||
)
|
||||
|
||||
const getTokenIndex = useCallback(
|
||||
(address) =>
|
||||
Object.entries(symbolsForMangoGroup).findIndex((x) => x[1] === address),
|
||||
[symbolsForMangoGroup]
|
||||
)
|
||||
|
||||
return {
|
||||
programId,
|
||||
marketList,
|
||||
spotMarkets,
|
||||
symbols,
|
||||
symbols: symbolsForMangoGroup,
|
||||
getTokenIndex,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,8 @@
|
|||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { ACCOUNT_LAYOUT, nativeToUi } from '@blockworks-foundation/mango-client'
|
||||
import { nativeToUi } from '@blockworks-foundation/mango-client'
|
||||
import { SRM_DECIMALS } from '@project-serum/serum/lib/token-instructions'
|
||||
import { getFeeTier, getFeeRates } from '@project-serum/serum'
|
||||
|
||||
function parseTokenAccountData(
|
||||
data: Buffer
|
||||
): { mint: PublicKey; owner: PublicKey; amount: number } {
|
||||
const { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data)
|
||||
return {
|
||||
mint: new PublicKey(mint),
|
||||
owner: new PublicKey(owner),
|
||||
amount,
|
||||
}
|
||||
}
|
||||
import { parseTokenAccountData } from '../utils/tokens'
|
||||
|
||||
const useSrmAccount = () => {
|
||||
const srmAccount = useMangoStore((s) => s.selectedMangoGroup.srmAccount)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useEffect } from 'react'
|
||||
import Wallet from '@project-serum/sol-wallet-adapter'
|
||||
// import { notify } from './notifications'
|
||||
import useLocalStorageState from './useLocalStorageState'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
|
||||
|
@ -14,6 +13,9 @@ export default function useWallet() {
|
|||
const setMangoStore = useMangoStore((state) => state.set)
|
||||
const { current: wallet, connected } = useMangoStore((state) => state.wallet)
|
||||
const endpoint = useMangoStore((state) => state.connection.endpoint)
|
||||
const fetchWalletBalances = useMangoStore(
|
||||
(s) => s.actions.fetchWalletBalances
|
||||
)
|
||||
const [savedProviderUrl] = useLocalStorageState(
|
||||
'walletProvider',
|
||||
'https://www.sollet.io'
|
||||
|
@ -30,6 +32,7 @@ export default function useWallet() {
|
|||
setMangoStore((state) => {
|
||||
state.wallet.current = newWallet
|
||||
})
|
||||
// eslint-disable-next-line
|
||||
}, [endpoint])
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -39,18 +42,6 @@ export default function useWallet() {
|
|||
state.wallet.connected = true
|
||||
})
|
||||
console.log('connected!')
|
||||
// const walletPublicKey = wallet.publicKey.toBase58()
|
||||
// const keyToDisplay =
|
||||
// walletPublicKey.length > 20
|
||||
// ? `${walletPublicKey.substring(0, 7)}.....${walletPublicKey.substring(
|
||||
// walletPublicKey.length - 7,
|
||||
// walletPublicKey.length
|
||||
// )}`
|
||||
// : walletPublicKey
|
||||
// notify({
|
||||
// message: 'Wallet update',
|
||||
// description: 'Connected to wallet ' + keyToDisplay,
|
||||
// })
|
||||
})
|
||||
wallet.on('disconnect', () => {
|
||||
setMangoStore((state) => {
|
||||
|
@ -58,11 +49,7 @@ export default function useWallet() {
|
|||
state.marginAccounts = []
|
||||
state.selectedMarginAccount.current = null
|
||||
})
|
||||
// notify({
|
||||
// message: 'Wallet update',
|
||||
// description: 'Disconnected from wallet',
|
||||
// })
|
||||
// localStorage.removeItem('feeDiscountKey')
|
||||
console.log('wallet disconnected')
|
||||
})
|
||||
return () => {
|
||||
wallet.disconnect()
|
||||
|
@ -72,5 +59,9 @@ export default function useWallet() {
|
|||
}
|
||||
}, [wallet])
|
||||
|
||||
useEffect(() => {
|
||||
fetchWalletBalances()
|
||||
}, [connected, fetchWalletBalances])
|
||||
|
||||
return { wallet, connected }
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
"pre-push": "yarn run type-check"
|
||||
}
|
||||
},
|
||||
|
@ -37,6 +36,8 @@
|
|||
"antd": "^4.15.0",
|
||||
"babel-plugin-import": "^1.13.3",
|
||||
"bn.js": "^5.2.0",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"immer": "^9.0.1",
|
||||
"immutable-tuple": "^0.4.10",
|
||||
"next": "latest",
|
||||
|
|
|
@ -8,9 +8,10 @@ import {
|
|||
MangoGroup,
|
||||
MarginAccount,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
import { AccountInfo, Connection } from '@solana/web3.js'
|
||||
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'
|
||||
import { Wallet } from '@project-serum/sol-wallet-adapter'
|
||||
import { EndpointInfo } from '../@types/types'
|
||||
import { getOwnedTokenAccounts } from '../utils/tokens'
|
||||
|
||||
export const ENDPOINTS: EndpointInfo[] = [
|
||||
{
|
||||
|
@ -28,7 +29,7 @@ export const ENDPOINTS: EndpointInfo[] = [
|
|||
const CLUSTER = 'mainnet-beta'
|
||||
const ENDPOINT_URL = ENDPOINTS.find((e) => e.name === CLUSTER).endpoint
|
||||
const DEFAULT_CONNECTION = new Connection(ENDPOINT_URL, 'recent')
|
||||
const DEFAULT_MANGO_GROUP = 'BTC_ETH_USDT'
|
||||
const DEFAULT_MANGO_GROUP_NAME = 'BTC_ETH_USDT'
|
||||
|
||||
interface AccountInfoList {
|
||||
[key: string]: AccountInfo<Buffer>
|
||||
|
@ -60,6 +61,7 @@ interface MangoStore extends State {
|
|||
markets: {
|
||||
[key: string]: Market
|
||||
}
|
||||
mintDecimals: number[]
|
||||
}
|
||||
selectedMarginAccount: {
|
||||
current: MarginAccount | null
|
||||
|
@ -72,12 +74,14 @@ interface MangoStore extends State {
|
|||
wallet: {
|
||||
connected: boolean
|
||||
current: Wallet
|
||||
balances: Array<{ account: any; publicKey: PublicKey }>
|
||||
}
|
||||
set: (x: any) => void
|
||||
actions: any
|
||||
}
|
||||
|
||||
const useMangoStore = create<MangoStore>(
|
||||
devtools((set) => ({
|
||||
devtools((set, get) => ({
|
||||
accountInfos: {},
|
||||
connection: {
|
||||
cluster: CLUSTER,
|
||||
|
@ -85,17 +89,18 @@ const useMangoStore = create<MangoStore>(
|
|||
endpoint: ENDPOINT_URL,
|
||||
},
|
||||
selectedMangoGroup: {
|
||||
name: DEFAULT_MANGO_GROUP,
|
||||
name: DEFAULT_MANGO_GROUP_NAME,
|
||||
current: null,
|
||||
markets: {},
|
||||
srmAccount: null,
|
||||
mintDecimals: [],
|
||||
},
|
||||
selectedMarket: {
|
||||
name: Object.entries(
|
||||
IDS[CLUSTER].mango_groups[DEFAULT_MANGO_GROUP].spot_market_symbols
|
||||
IDS[CLUSTER].mango_groups[DEFAULT_MANGO_GROUP_NAME].spot_market_symbols
|
||||
)[0][0],
|
||||
address: Object.entries(
|
||||
IDS[CLUSTER].mango_groups[DEFAULT_MANGO_GROUP].spot_market_symbols
|
||||
IDS[CLUSTER].mango_groups[DEFAULT_MANGO_GROUP_NAME].spot_market_symbols
|
||||
)[0][1],
|
||||
},
|
||||
market: {
|
||||
|
@ -118,8 +123,32 @@ const useMangoStore = create<MangoStore>(
|
|||
wallet: {
|
||||
connected: false,
|
||||
current: null,
|
||||
balances: [],
|
||||
},
|
||||
set: (fn) => set(produce(fn)),
|
||||
actions: {
|
||||
async fetchWalletBalances() {
|
||||
const connection = get().connection.current
|
||||
const wallet = get().wallet.current
|
||||
const connected = get().wallet.connected
|
||||
const set = get().set
|
||||
console.log('fetchingWalletBalances', connected, wallet)
|
||||
if (wallet && connected) {
|
||||
const ownerAddress = wallet.publicKey
|
||||
const ownedTokenAccounts = await getOwnedTokenAccounts(
|
||||
connection,
|
||||
ownerAddress
|
||||
)
|
||||
set((state) => {
|
||||
state.wallet.balances = ownedTokenAccounts
|
||||
})
|
||||
} else {
|
||||
set((state) => {
|
||||
state.wallet.balances = []
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
|
|
|
@ -24,5 +24,11 @@
|
|||
]
|
||||
},
|
||||
"exclude": ["node_modules", ".next", "out", "public/datafeeds/udf/src"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js",
|
||||
"components/AccountSelect.jsx"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,262 +0,0 @@
|
|||
import { useEffect, useReducer } from 'react'
|
||||
|
||||
import assert from 'assert'
|
||||
|
||||
const pageLoadTime = new Date()
|
||||
|
||||
const globalCache: Map<any, any> = new Map()
|
||||
|
||||
class FetchLoopListener<T = any> {
|
||||
cacheKey: any
|
||||
fn: () => Promise<T>
|
||||
refreshInterval: number
|
||||
refreshIntervalOnError: number | null
|
||||
callback: () => void
|
||||
cacheNullValues = true
|
||||
|
||||
constructor(
|
||||
cacheKey: any,
|
||||
fn: () => Promise<T>,
|
||||
refreshInterval: number,
|
||||
refreshIntervalOnError: number | null,
|
||||
callback: () => void,
|
||||
cacheNullValues: boolean
|
||||
) {
|
||||
this.cacheKey = cacheKey
|
||||
this.fn = fn
|
||||
this.refreshInterval = refreshInterval
|
||||
this.refreshIntervalOnError = refreshIntervalOnError
|
||||
this.callback = callback
|
||||
this.cacheNullValues = cacheNullValues
|
||||
}
|
||||
}
|
||||
|
||||
class FetchLoopInternal<T = any> {
|
||||
cacheKey: any
|
||||
fn: () => Promise<T>
|
||||
timeoutId: null | any
|
||||
listeners: Set<FetchLoopListener<T>>
|
||||
errors: number
|
||||
cacheNullValues = true
|
||||
|
||||
constructor(cacheKey: any, fn: () => Promise<T>, cacheNullValues: boolean) {
|
||||
this.cacheKey = cacheKey
|
||||
this.fn = fn
|
||||
this.timeoutId = null
|
||||
this.listeners = new Set()
|
||||
this.errors = 0
|
||||
this.cacheNullValues = cacheNullValues
|
||||
}
|
||||
|
||||
get refreshInterval(): number {
|
||||
return Math.min(
|
||||
...[...this.listeners].map((listener) => listener.refreshInterval)
|
||||
)
|
||||
}
|
||||
|
||||
get refreshIntervalOnError(): number | null {
|
||||
const refreshIntervalsOnError: number[] = [...this.listeners]
|
||||
.map((listener) => listener.refreshIntervalOnError)
|
||||
.filter((x): x is number => x !== null)
|
||||
if (refreshIntervalsOnError.length === 0) {
|
||||
return null
|
||||
}
|
||||
return Math.min(...refreshIntervalsOnError)
|
||||
}
|
||||
|
||||
get stopped(): boolean {
|
||||
return this.listeners.size === 0
|
||||
}
|
||||
|
||||
addListener(listener: FetchLoopListener<T>): void {
|
||||
const previousRefreshInterval = this.refreshInterval
|
||||
this.listeners.add(listener)
|
||||
if (this.refreshInterval < previousRefreshInterval) {
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
removeListener(listener: FetchLoopListener<T>): void {
|
||||
assert(this.listeners.delete(listener))
|
||||
if (this.stopped) {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId)
|
||||
this.timeoutId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners(): void {
|
||||
this.listeners.forEach((listener) => listener.callback())
|
||||
}
|
||||
|
||||
refresh = async () => {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId)
|
||||
this.timeoutId = null
|
||||
}
|
||||
if (this.stopped) {
|
||||
return
|
||||
}
|
||||
|
||||
let errored = false
|
||||
try {
|
||||
const data = await this.fn()
|
||||
if (!this.cacheNullValues && data === null) {
|
||||
console.log(`Not caching null value for ${this.cacheKey}`)
|
||||
// cached data has not changed so no need to re-render
|
||||
this.errors = 0
|
||||
return data
|
||||
} else {
|
||||
globalCache.set(this.cacheKey, data)
|
||||
this.errors = 0
|
||||
this.notifyListeners()
|
||||
return data
|
||||
}
|
||||
} catch (error) {
|
||||
++this.errors
|
||||
console.warn(error)
|
||||
errored = true
|
||||
} finally {
|
||||
if (!this.timeoutId && !this.stopped) {
|
||||
let waitTime = this.refreshInterval
|
||||
if (
|
||||
errored &&
|
||||
this.refreshIntervalOnError &&
|
||||
this.refreshIntervalOnError > 0
|
||||
) {
|
||||
waitTime = this.refreshIntervalOnError
|
||||
}
|
||||
|
||||
// Back off on errors.
|
||||
if (this.errors > 0) {
|
||||
waitTime = Math.min(1000 * 2 ** (this.errors - 1), 60000)
|
||||
}
|
||||
|
||||
// Don't do any refreshing for the first five seconds, to make way for other things to load.
|
||||
const timeSincePageLoad = +new Date() - +pageLoadTime
|
||||
if (timeSincePageLoad < 5000) {
|
||||
waitTime += 5000 - timeSincePageLoad / 2
|
||||
}
|
||||
|
||||
// Refresh background pages slowly.
|
||||
if (document.visibilityState === 'hidden') {
|
||||
waitTime = 60000
|
||||
} else if (!document.hasFocus()) {
|
||||
waitTime *= 1.5
|
||||
}
|
||||
|
||||
// Add jitter so we don't send all requests at the same time.
|
||||
waitTime *= 0.8 + 0.4 * Math.random()
|
||||
|
||||
this.timeoutId = setTimeout(this.refresh, waitTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FetchLoops {
|
||||
loops = new Map()
|
||||
|
||||
addListener<T>(listener: FetchLoopListener<T>) {
|
||||
if (!this.loops.has(listener.cacheKey)) {
|
||||
this.loops.set(
|
||||
listener.cacheKey,
|
||||
new FetchLoopInternal<T>(
|
||||
listener.cacheKey,
|
||||
listener.fn,
|
||||
listener.cacheNullValues
|
||||
)
|
||||
)
|
||||
}
|
||||
this.loops.get(listener.cacheKey).addListener(listener)
|
||||
}
|
||||
|
||||
removeListener<T>(listener: FetchLoopListener<T>) {
|
||||
const loop = this.loops.get(listener.cacheKey)
|
||||
loop.removeListener(listener)
|
||||
if (loop.stopped) {
|
||||
this.loops.delete(listener.cacheKey)
|
||||
globalCache.delete(listener.cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
refresh(cacheKey) {
|
||||
if (this.loops.has(cacheKey)) {
|
||||
this.loops.get(cacheKey).refresh()
|
||||
}
|
||||
}
|
||||
|
||||
refreshAll() {
|
||||
return Promise.all([...this.loops.values()].map((loop) => loop.refresh()))
|
||||
}
|
||||
}
|
||||
const globalLoops = new FetchLoops()
|
||||
|
||||
export function useAsyncData<T = any>(
|
||||
asyncFn: () => Promise<T>,
|
||||
cacheKey: any,
|
||||
{ refreshInterval = 60000, refreshIntervalOnError = null } = {},
|
||||
cacheNullValues = true
|
||||
): [null | undefined | T, boolean] {
|
||||
const [, rerender] = useReducer((i) => i + 1, 0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!cacheKey) {
|
||||
return () => {}
|
||||
}
|
||||
const listener = new FetchLoopListener<T>(
|
||||
cacheKey,
|
||||
asyncFn,
|
||||
refreshInterval,
|
||||
refreshIntervalOnError,
|
||||
rerender,
|
||||
cacheNullValues
|
||||
)
|
||||
globalLoops.addListener(listener)
|
||||
return () => globalLoops.removeListener(listener)
|
||||
// eslint-disable-next-line
|
||||
}, [cacheKey, refreshInterval])
|
||||
|
||||
if (!cacheKey) {
|
||||
return [null, false]
|
||||
}
|
||||
|
||||
const loaded = globalCache.has(cacheKey)
|
||||
const data = loaded ? globalCache.get(cacheKey) : undefined
|
||||
return [data, loaded]
|
||||
}
|
||||
|
||||
export function refreshCache(cacheKey: any, clearCache = false): void {
|
||||
if (clearCache) {
|
||||
globalCache.delete(cacheKey)
|
||||
}
|
||||
const loop = globalLoops.loops.get(cacheKey)
|
||||
if (loop) {
|
||||
loop.refresh()
|
||||
if (clearCache) {
|
||||
loop.notifyListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshAllCaches(): void {
|
||||
for (const loop of globalLoops.loops.values()) {
|
||||
loop.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
export function setCache(
|
||||
cacheKey: any,
|
||||
value: any,
|
||||
{ initializeOnly = false } = {}
|
||||
): void {
|
||||
if (initializeOnly && globalCache.has(cacheKey)) {
|
||||
return
|
||||
}
|
||||
globalCache.set(cacheKey, value)
|
||||
const loop = globalLoops.loops.get(cacheKey)
|
||||
if (loop) {
|
||||
loop.notifyListeners()
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { TOKEN_MINTS } from '@project-serum/serum'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import BN from 'bn.js'
|
||||
|
||||
|
@ -76,7 +77,7 @@ export function getTokenMultiplierFromDecimals(decimals: number): BN {
|
|||
return new BN(10).pow(new BN(decimals))
|
||||
}
|
||||
|
||||
export function abbreviateAddress(address: PublicKey, size = 4) {
|
||||
export function abbreviateAddress(address: PublicKey, size = 5) {
|
||||
const base58 = address.toBase58()
|
||||
return base58.slice(0, size) + '…' + base58.slice(-size)
|
||||
}
|
||||
|
@ -143,3 +144,7 @@ export const tokenPrecision = {
|
|||
USDT: 2,
|
||||
WUSDT: 2,
|
||||
}
|
||||
|
||||
export const getSymbolForTokenMintAddress = (address: string): string => {
|
||||
return TOKEN_MINTS.find((m) => m.address.toString() === address).name
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export function notify(args) {
|
||||
alert(`notify: ${args}`)
|
||||
console.log(`-=-notify:`, args)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { ACCOUNT_LAYOUT } from '@blockworks-foundation/mango-client'
|
||||
import { Connection, PublicKey } from '@solana/web3.js'
|
||||
import * as bs58 from 'bs58'
|
||||
import { AccountInfo as TokenAccount } from '@solana/spl-token'
|
||||
import { TokenInstructions } from '@project-serum/serum'
|
||||
|
||||
export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
||||
)
|
||||
|
||||
export type ProgramAccount<T> = {
|
||||
publicKey: PublicKey
|
||||
account: T
|
||||
}
|
||||
|
||||
export function parseTokenAccountData(
|
||||
data: Buffer
|
||||
): { mint: PublicKey; owner: PublicKey; amount: number } {
|
||||
const { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data)
|
||||
return {
|
||||
mint: new PublicKey(mint),
|
||||
owner: new PublicKey(owner),
|
||||
amount,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOwnedTokenAccounts(
|
||||
connection: Connection,
|
||||
publicKey: PublicKey
|
||||
): Promise<ProgramAccount<TokenAccount>[]> {
|
||||
const filters = getOwnedAccountsFilters(publicKey)
|
||||
// @ts-ignore
|
||||
const resp = await connection._rpcRequest('getProgramAccounts', [
|
||||
TokenInstructions.TOKEN_PROGRAM_ID.toBase58(),
|
||||
{
|
||||
commitment: connection.commitment,
|
||||
filters,
|
||||
},
|
||||
])
|
||||
if (resp.error) {
|
||||
throw new Error(
|
||||
'failed to get token accounts owned by ' +
|
||||
publicKey.toBase58() +
|
||||
': ' +
|
||||
resp.error.message
|
||||
)
|
||||
}
|
||||
return resp.result.map(({ pubkey, account: { data } }) => {
|
||||
data = bs58.decode(data)
|
||||
return {
|
||||
publicKey: new PublicKey(pubkey),
|
||||
account: parseTokenAccountData(data),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getOwnedAccountsFilters(publicKey: PublicKey) {
|
||||
return [
|
||||
{
|
||||
memcmp: {
|
||||
offset: ACCOUNT_LAYOUT.offsetOf('owner'),
|
||||
bytes: publicKey.toBase58(),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataSize: ACCOUNT_LAYOUT.span,
|
||||
},
|
||||
]
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { PublicKey } from '@solana/web3.js'
|
||||
|
||||
export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
||||
)
|
Loading…
Reference in New Issue