deposit modal

This commit is contained in:
Tyler Shipe 2021-04-09 20:01:00 -04:00
parent b5562c161c
commit 71db71b347
17 changed files with 454 additions and 399 deletions

View File

@ -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>
</>

View File

@ -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)

28
components/Loading.tsx Normal file
View File

@ -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

View File

@ -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}
/>
)}
</>

View File

@ -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>
)

View File

@ -40,6 +40,7 @@ const useMarginAccount = () => {
setMangoStore((state) => {
state.selectedMangoGroup.current = mangoGroup
state.selectedMangoGroup.srmAccount = srmAccountInfo
state.selectedMangoGroup.mintDecimals = mangoGroup.mintDecimals
})
})
.catch((err) => {

View File

@ -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,
}
}

View File

@ -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)

View File

@ -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 }
}

View File

@ -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",

View File

@ -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 = []
})
}
},
},
}))
)

View File

@ -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"
]
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -1,3 +1,3 @@
export function notify(args) {
alert(`notify: ${args}`)
console.log(`-=-notify:`, args)
}

69
utils/tokens.ts Normal file
View File

@ -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,
},
]
}

View File

@ -1,5 +0,0 @@
import { PublicKey } from '@solana/web3.js'
export const TOKEN_PROGRAM_ID = new PublicKey(
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
)