mango v4 sandbox

This commit is contained in:
tjs 2022-05-04 00:20:14 -04:00
parent 8a9a787f72
commit c8898467db
25 changed files with 3091 additions and 214 deletions

6
.eslintignore Normal file
View File

@ -0,0 +1,6 @@
**/node_modules/*
**/out/*
**/.next/*
**/public/charting_library/*
**/public/datafeeds/*
**/components/charting_library/*

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
.serverless/
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.env
# dependencies
/node_modules

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
node_modules
.next
yarn.lock
package-lock.json
public
components/charting_library

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

View File

@ -0,0 +1,82 @@
import React, { useState } from 'react'
import mangoStore from '../store/state'
import Button from './shared/Button'
import Loading from './shared/Loading'
import Modal from './shared/Modal'
type DepositModalProps = {
isOpen: boolean
onClose: () => void
}
function DepositModal({ isOpen, onClose }: DepositModalProps) {
const [inputAmount, setInputAmount] = useState('')
const [submitting, setSubmitting] = useState(false)
const [selectedToken, setSelectedToken] = useState('USDC')
const handleDeposit = async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const actions = mangoStore.getState().actions
const mangoAccount = mangoStore.getState().mangoAccount
if (!mangoAccount || !group) return
console.log(1)
setSubmitting(true)
const tx = await client.deposit(
group,
mangoAccount,
selectedToken,
parseFloat(inputAmount)
)
console.log(2, tx)
await actions.reloadAccount()
setSubmitting(false)
console.log(3)
onClose()
}
const handleTokenSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedToken(e.target.value)
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div>
<div className="relative mt-1 rounded-md shadow-sm">
<div className="absolute inset-y-0 left-0 flex items-center">
<label htmlFor="token" className="sr-only">
Token
</label>
<select
id="token"
name="token"
autoComplete="token"
className="h-full rounded-md border-transparent bg-transparent py-0 pl-3 pr-7 text-gray-500 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
onChange={handleTokenSelect}
>
<option>USDC</option>
<option>BTC</option>
</select>
</div>
<input
type="text"
name="deposit"
id="deposit"
className="block w-full rounded-md border-gray-300 pl-24 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="0.00"
value={inputAmount}
onChange={(e) => setInputAmount(e.target.value)}
/>
</div>
</div>
<div className="mt-4 flex justify-center">
<Button onClick={handleDeposit} className="flex items-center">
{submitting ? <Loading className="mr-2 h-5 w-5" /> : null} Deposit
</Button>
</div>
</Modal>
)
}
export default DepositModal

View File

@ -0,0 +1,55 @@
import mangoStore from '../store/state'
import ExplorerLink from './shared/ExplorerLink'
const MangoAccount = () => {
const mangoAccount = mangoStore((s) => s.mangoAccount)
const group = mangoStore((s) => s.group)
if (!mangoAccount) return null
const activeTokens = mangoAccount
? mangoAccount.tokens.filter((ta) => ta.isActive())
: []
const banks = group?.banksMap
? Array.from(group?.banksMap, ([key, value]) => ({ key, value }))
: []
return (
<div key={mangoAccount.publicKey.toString()}>
<div
key={mangoAccount?.publicKey.toString()}
className="rounded border p-4"
>
Mango Account:{' '}
<ExplorerLink address={mangoAccount?.publicKey.toString()} />
{activeTokens.map((ta, idx) => {
return (
<div key={idx} className="mt-2 rounded border p-2">
<div>Token Index {ta.tokenIndex}</div>
<div>Indexed Value {ta.indexedValue.toNumber()}</div>
<div>In Use Count {ta.inUseCount}</div>
</div>
)
})}
<div className="mt-2 space-y-2 rounded border p-2">
{banks.map((bank) => {
return (
<div key={bank.key}>
<div>
Deposit:{' '}
{mangoAccount.getNativeDeposit(bank.value).toNumber()}
</div>
<div>
Borrows: {mangoAccount.getNativeBorrow(bank.value).toNumber()}
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
export default MangoAccount

204
components/SerumOrder.tsx Normal file
View File

@ -0,0 +1,204 @@
import { DEVNET_SERUM3_PROGRAM_ID } from '@blockworks-foundation/mango-v4'
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '@blockworks-foundation/mango-v4/dist/accounts/serum3'
import { Order } from '@blockworks-foundation/mango-v4/node_modules/@project-serum/serum/lib/market'
import { useState } from 'react'
import mangoStore from '../store/state'
import Button from './shared/Button'
import ExplorerLink from './shared/ExplorerLink'
const SerumOrder = () => {
const markets = mangoStore((s) => s.markets)
const serumOrders = mangoStore((s) => s.serumOrders)
const actions = mangoStore.getState().actions
const mangoAccount = mangoStore.getState().mangoAccount
console.log('mangoAccount', mangoAccount)
const [tradeForm, setTradeForm] = useState({ side: '', size: '', price: '' })
const handlePlaceOrder = async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount
if (!group || !mangoAccount) return
try {
const side = tradeForm.side === 'buy' ? Serum3Side.bid : Serum3Side.ask
const tx = await client.serum3PlaceOrder(
group,
mangoAccount,
DEVNET_SERUM3_PROGRAM_ID,
'BTC/USDC',
side,
parseFloat(tradeForm.price),
parseFloat(tradeForm.size),
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
10
)
console.log('tx', tx)
actions.reloadAccount()
actions.loadSerumMarket()
} catch (e) {
console.log('Error placing order:', e)
}
}
const cancelOrder = async (order: Order) => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount
if (!group || !mangoAccount) return
try {
const tx = await client.serum3CancelOrder(
group,
mangoAccount,
DEVNET_SERUM3_PROGRAM_ID,
'BTC/USDC',
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId
)
actions.reloadAccount()
actions.loadSerumMarket()
console.log('tx', tx)
} catch (e) {
console.log('error cancelling order', e)
}
}
return (
<div className="rounded border p-4">
Serum 3
<div className="rounded border p-2">
{markets?.map((m) => {
return (
<div key={m.name}>
<div>
{m.name}: <ExplorerLink address={m.publicKey.toString()} />
</div>
<div>Market Index: {m.marketIndex}</div>
<div>
{serumOrders?.map((o) => {
const ooAddress = o.openOrdersAddress
const myOrder = mangoAccount?.serum3
.map((s) => s.openOrders.toString())
.includes(ooAddress.toString())
return (
<div
key={`${o.side}${o.size}${o.price}`}
className="my-1 rounded border p-2"
>
<div className="flex items-center justify-between">
<div>
<div>Side: {o.side}</div>
<div>Size: {o.size}</div>
<div>Price: {o.price}</div>
</div>
{myOrder ? (
<div>
<Button onClick={() => cancelOrder(o)}>
Cancel
</Button>
</div>
) : null}
</div>
</div>
)
})}
</div>
</div>
)
})}
</div>
<form className="mt-4">
<div>
<label
htmlFor="side"
className="block text-sm font-medium text-gray-700"
>
Side
</label>
<div className="mt-1">
<input
type="text"
name="side"
id="side"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="buy"
value={tradeForm.side}
onChange={(e) =>
setTradeForm((prevState) => ({
...prevState,
side: e.target.value,
}))
}
/>
</div>
</div>
<div>
<label
htmlFor="size"
className="block text-sm font-medium text-gray-700"
>
Size
</label>
<div className="mt-1">
<input
type="number"
name="size"
id="size"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="0.00"
value={tradeForm.size}
onChange={(e) =>
setTradeForm((prevState) => ({
...prevState,
size: e.target.value,
}))
}
/>
</div>
</div>
<div>
<label
htmlFor="price"
className="block text-sm font-medium text-gray-700"
>
Price
</label>
<div className="mt-1">
<input
type="number"
name="price"
id="price"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="0.00"
value={tradeForm.price}
onChange={(e) =>
setTradeForm((prevState) => ({
...prevState,
price: e.target.value,
}))
}
/>
</div>
</div>
</form>
<div className="mt-4 flex justify-center">
<Button onClick={handlePlaceOrder}>Place Order</Button>
</div>
</div>
)
}
export default SerumOrder

53
components/TopBar.tsx Normal file
View File

@ -0,0 +1,53 @@
// Default styles that can be overridden by your app
require('@solana/wallet-adapter-react-ui/styles.css')
import { useWallet } from '@solana/wallet-adapter-react'
import {
WalletDisconnectButton,
WalletMultiButton,
} from '@solana/wallet-adapter-react-ui'
import { useState } from 'react'
import Button from './shared/Button'
import DepositModal from './DepositModal'
import WithdrawModal from './WithdrawModal'
const TopBar = () => {
const [showDepositModal, setShowDepositModal] = useState(false)
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
const { connected } = useWallet()
return (
<>
<div className="flex w-full p-2">
<div className="ml-auto">
<div className="flex space-x-2">
{connected ? (
<>
<Button onClick={() => setShowDepositModal(true)}>
Deposit
</Button>
<Button onClick={() => setShowWithdrawModal(true)}>
Withdraw
</Button>
</>
) : null}
{connected ? <WalletDisconnectButton /> : <WalletMultiButton />}
</div>
</div>
</div>
{showDepositModal ? (
<DepositModal
isOpen={showDepositModal}
onClose={() => setShowDepositModal(false)}
/>
) : null}
{showWithdrawModal ? (
<WithdrawModal
isOpen={showWithdrawModal}
onClose={() => setShowWithdrawModal(false)}
/>
) : null}
</>
)
}
export default TopBar

View File

@ -0,0 +1,81 @@
import { useState } from 'react'
import mangoStore from '../store/state'
import Button from './shared/Button'
import Loading from './shared/Loading'
import Modal from './shared/Modal'
type WithdrawModalProps = {
isOpen: boolean
onClose: () => void
}
function WithdrawModal({ isOpen, onClose }: WithdrawModalProps) {
const [inputAmount, setInputAmount] = useState('')
const [submitting, setSubmitting] = useState(false)
const [selectedToken, setSelectedToken] = useState('USDC')
const handleWithdraw = async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount
const actions = mangoStore.getState().actions
if (!mangoAccount || !group) return
setSubmitting(true)
const tx = await client.withdraw(
group,
mangoAccount,
selectedToken,
parseFloat(inputAmount),
false
)
console.log('tx: ', tx)
await actions.reloadAccount()
setSubmitting(false)
onClose()
}
const handleTokenSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedToken(e.target.value)
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div>
<div className="relative mt-1 rounded-md shadow-sm">
<div className="absolute inset-y-0 left-0 flex items-center">
<label htmlFor="token" className="sr-only">
Token
</label>
<select
id="token"
name="token"
autoComplete="token"
className="h-full rounded-md border-transparent bg-transparent py-0 pl-3 pr-7 text-gray-500 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
onChange={handleTokenSelect}
>
<option>USDC</option>
<option>BTC</option>
</select>
</div>
<input
type="text"
name="withdraw"
id="withdraw"
className="block w-full rounded-md border-gray-300 pl-24 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="0.00"
value={inputAmount}
onChange={(e) => setInputAmount(e.target.value)}
/>
</div>
</div>
<div className="mt-4 flex justify-center">
<Button onClick={handleWithdraw} className="flex items-center">
{submitting ? <Loading className="mr-2 h-5 w-5" /> : null} Withdraw
</Button>
</div>
</Modal>
)
}
export default WithdrawModal

View File

@ -0,0 +1,31 @@
import { FunctionComponent, ReactNode } from 'react'
interface ButtonProps {
onClick?: (e?: React.MouseEvent) => void
disabled?: boolean
className?: string
primary?: boolean
children?: ReactNode
}
const Button: FunctionComponent<ButtonProps> = ({
children,
onClick,
disabled = false,
className,
...props
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={`whitespace-nowrap rounded-full bg-orange-600 px-6 py-2 font-bold text-orange-100 hover:brightness-[1.1] focus:outline-none
disabled:cursor-not-allowed disabled:hover:brightness-100 ${className}`}
{...props}
>
{children}
</button>
)
}
export default Button

View File

@ -0,0 +1,21 @@
type ExplorerLinkProps = {
address: string
}
const ExplorerLink = ({ address }: ExplorerLinkProps) => {
const cluster = 'devnet'
return (
<a
href={
'https://explorer.solana.com/address/' + address + '?cluster=' + cluster
}
className="ml-1 text-blue-400 hover:underline"
target="_blank"
rel="noreferrer"
>
{address}
</a>
)
}
export default ExplorerLink

View File

@ -0,0 +1,26 @@
const Loading = ({ className = '' }) => {
return (
<svg
className={`${className} h-5 w-5 animate-spin`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className={`opacity-25`}
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className={`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

@ -0,0 +1,33 @@
import { useState, useRef } from 'react'
import { Dialog } from '@headlessui/react'
type ModalProps = {
title?: string
children: React.ReactNode
isOpen: boolean
onClose: (x: boolean) => void
}
function Modal({ title = '', children, isOpen, onClose }: ModalProps) {
return (
<Dialog
open={isOpen}
onClose={onClose}
className="fixed inset-0 z-10 overflow-y-auto"
>
<div className="min-h-screen px-4 text-center">
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />
<span className="inline-block h-screen align-middle" aria-hidden="true">
&#8203;
</span>
<div className="my-8 inline-block w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title>{title}</Dialog.Title>
{children}
</div>
</div>
</Dialog>
)
}
export default Modal

View File

@ -0,0 +1,25 @@
import { useEffect } from 'react'
import { useWallet } from '@solana/wallet-adapter-react'
import mangoStore from '../../store/state'
import { Wallet as AnchorWallet, Wallet } from '@project-serum/anchor'
const WalletListener = () => {
const actions = mangoStore((s) => s.actions)
const { wallet, publicKey } = useWallet()
useEffect(() => {
const onConnect = async () => {
if (!wallet) return
console.log('onConnect pk:', publicKey)
actions.connectWallet(wallet.adapter as unknown as Wallet)
}
if (publicKey) {
onConnect()
}
}, [wallet?.adapter, publicKey])
return null
}
export default WalletListener

View File

@ -1,6 +1,25 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
env: {
BROWSER: true,
},
webpack: (config, { isServer }) => {
if (!isServer) {
// don't resolve 'fs' module on the client to prevent this error on build --> Error: Can't resolve 'fs'
config.resolve.fallback = {
fs: false,
os: false,
path: false,
process: false,
util: false,
assert: false,
stream: false,
}
}
return config
},
}
module.exports = nextConfig

View File

@ -9,16 +9,39 @@
"lint": "next lint"
},
"dependencies": {
"@blockworks-foundation/mango-v4": "git+https://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:x-oauth-basic@github.com/blockworks-foundation/mango-v4.git",
"@headlessui/react": "^1.5.0",
"@heroicons/react": "^1.0.6",
"@solana/wallet-adapter-base": "^0.9.5",
"@solana/wallet-adapter-react": "^0.15.4",
"@solana/wallet-adapter-react-ui": "^0.9.6",
"@solana/wallet-adapter-wallets": "^0.16.0",
"@tailwindcss/forms": "^0.5.0",
"immer": "^9.0.12",
"next": "12.1.5",
"process": "^0.11.10",
"react": "18.0.0",
"react-dom": "18.0.0"
"react-dom": "18.0.0",
"zustand": "^3.7.2"
},
"peerDependencies": {
"@project-serum/anchor": "^0.22.0"
},
"devDependencies": {
"@types/bn.js": "4.11.6",
"@types/node": "17.0.23",
"@types/react": "18.0.3",
"@types/react-dom": "18.0.0",
"autoprefixer": "^10.4.4",
"eslint": "8.13.0",
"eslint-config-next": "12.1.5",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"postcss": "^8.4.12",
"prettier": "^2.6.2",
"prettier-plugin-tailwindcss": "^0.1.8",
"tailwindcss": "^3.0.24",
"typescript": "4.6.3"
}
}

View File

@ -1,70 +1,123 @@
import { useEffect, useMemo } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import { MANGO_V4_ID } from '@blockworks-foundation/mango-v4'
import {
ConnectionProvider,
WalletProvider,
} from '@solana/wallet-adapter-react'
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'
import {
GlowWalletAdapter,
PhantomWalletAdapter,
SolflareWalletAdapter,
} from '@solana/wallet-adapter-wallets'
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'
import { clusterApiUrl } from '@solana/web3.js'
import mangoStore from '../store/state'
import TopBar from '../components/TopBar'
import WalletListener from '../components/wallet/WalletListener'
import SerumOrder from '../components/SerumOrder'
import ExplorerLink from '../components/shared/ExplorerLink'
import MangoAccount from '../components/MangoAccount'
const hydrateStore = async () => {
const actions = mangoStore.getState().actions
actions.fetchGroup()
}
const Home: NextPage = () => {
const group = mangoStore((s) => s.group)
const network = WalletAdapterNetwork.Devnet
const endpoint = useMemo(() => clusterApiUrl(network), [network])
const banks = group?.banksMap
? Array.from(group?.banksMap, ([key, value]) => ({ key, value }))
: []
useEffect(() => {
hydrateStore()
}, [])
const wallets = useMemo(
() => [
new PhantomWalletAdapter(),
new GlowWalletAdapter(),
new SolflareWalletAdapter({ network }),
],
[network]
)
if (!group) return <div>Loading...</div>
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="">
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<WalletListener />
<TopBar />
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<div className="">
<div className="my-2 flex text-lg">
<div className="mx-auto">Mango V4 Devnet</div>
</div>
<div className="flex-col space-y-4">
<div className="flex">
<div className="mx-auto rounded border p-4">
Program: <ExplorerLink address={MANGO_V4_ID.toString()} />
</div>
</div>
<div className="flex">
<div className="mx-auto rounded border p-4">
Group:{' '}
<ExplorerLink address={group?.publicKey.toString()} />
{banks.map((bank) => {
return (
<div key={bank.key} className="mt-2 rounded border p-4">
<div>{bank.key}</div>
<div className="flex">
Mint:{' '}
<ExplorerLink
address={bank.value.mint.toString()}
/>
</div>
<div className="flex">
Oracle:{' '}
<ExplorerLink
address={bank.value.oracle.toString()}
/>
{/* Oracle Price: {bank.value.oraclePrice} */}
</div>
<div className="flex">
Vault:{' '}
<ExplorerLink
address={bank.value.vault.toString()}
/>
</div>
<div>
Vault Balance: {bank.value.depositIndex.toString()}
</div>
<div>
Deposit Index: {bank.value.depositIndex.toString()}
</div>
<div>
Borrow Index: {bank.value.borrowIndex.toString()}
</div>
</div>
)
})}
</div>
<MangoAccount />
<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.tsx</code>
</p>
<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h2>Documentation &rarr;</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className={styles.card}>
<h2>Learn &rarr;</h2>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/vercel/next.js/tree/canary/examples"
className={styles.card}
>
<h2>Examples &rarr;</h2>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h2>Deploy &rarr;</h2>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<span className={styles.logo}>
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
</span>
</a>
</footer>
<div className="mx-auto">
<SerumOrder />
</div>
</div>
</div>
</div>
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
</div>
)
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

144
store/state.ts Normal file
View File

@ -0,0 +1,144 @@
import create from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
import produce from 'immer'
import { Provider, Wallet } from '@project-serum/anchor'
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
import {
MangoClient,
DEVNET_GROUP,
Group,
MangoAccount,
Serum3Market,
DEVNET_SERUM3_PROGRAM_ID,
} from '@blockworks-foundation/mango-v4'
import EmptyWallet from '../utils/wallet'
import { Order } from '@blockworks-foundation/mango-v4/node_modules/@project-serum/serum/lib/market'
const connection = new Connection('https://api.devnet.solana.com', 'processed')
const options = Provider.defaultOptions() // use Provider instead of Provider
const provider = new Provider(
connection,
new EmptyWallet(Keypair.generate()),
options
)
export type MangoStore = {
connected: boolean
group: Group | undefined
client: MangoClient
mangoAccount: MangoAccount | undefined
markets: Serum3Market[] | undefined
serumOrders: Order[] | undefined
set: (x: (x: MangoStore) => void) => void
actions: {
fetchGroup: () => Promise<void>
connectWallet: (wallet: Wallet) => Promise<void>
reloadAccount: () => Promise<void>
loadSerumMarket: () => Promise<void>
}
}
const mangoStore = create<MangoStore>(
subscribeWithSelector((set, get) => {
return {
connected: false,
group: undefined,
client: MangoClient.connect(provider, true),
mangoAccount: undefined,
markets: undefined,
serumOrders: undefined,
set: (fn) => set(produce(fn)),
actions: {
fetchGroup: async () => {
try {
const client = get().client
const group = await client.getGroup(new PublicKey(DEVNET_GROUP))
const markets = await client.serum3GetMarket(
group,
group.banksMap.get('BTC')?.tokenIndex,
group.banksMap.get('USDC')?.tokenIndex
)
set((state) => {
state.connected = true
state.group = group
state.markets = markets
})
} catch (e) {
console.error('Error fetching group', e)
}
},
connectWallet: async (wallet) => {
try {
const group = get().group
if (!group) return
const provider = new Provider(connection, wallet, options)
const client = await MangoClient.connect(provider, true)
const mangoAccount = await client.getOrCreateMangoAccount(
group,
wallet.publicKey,
0,
'Account'
)
let orders = await client.getSerum3Orders(
group,
DEVNET_SERUM3_PROGRAM_ID,
'BTC/USDC'
)
set((state) => {
state.client = client
state.mangoAccount = mangoAccount
state.serumOrders = orders
})
} catch (e) {
console.error('Error fetching mango acct', e)
}
},
reloadAccount: async () => {
const client = get().client
const mangoAccount = get().mangoAccount
if (!mangoAccount) return
try {
const newMangoAccount = await client.getMangoAccount(mangoAccount)
set((state) => {
state.mangoAccount = newMangoAccount
})
} catch {
console.error('Error reloading mango account')
}
},
loadSerumMarket: async () => {
const client = get().client
const group = get().group
if (!group) return
const markets = await client.serum3GetMarket(
group,
group.banksMap.get('BTC')?.tokenIndex,
group.banksMap.get('USDC')?.tokenIndex
)
let orders = await client.getSerum3Orders(
group,
DEVNET_SERUM3_PROGRAM_ID,
'BTC/USDC'
)
set((state) => {
state.markets = markets
state.serumOrders = orders
})
},
},
}
})
)
export default mangoStore

View File

@ -1,116 +0,0 @@
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

View File

@ -1,16 +1,3 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

10
tailwind.config.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
content: ['./pages/**/*.tsx', './components/**/*.tsx'],
theme: {
extend: {},
},
plugins: [
// ...
require('@tailwindcss/forms'),
],
}

5
types/anchor.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
// import * as anchor from '@project-serum/anchor'
// declare module '@project-serum/anchor' {
// export const workspace: any
// export const Wallet: import('@project-serum/anchor/dist/cjs/nodewallet').default
// }

22
utils/wallet.ts Normal file
View File

@ -0,0 +1,22 @@
import { Wallet } from '@project-serum/anchor'
import { Keypair, PublicKey, Transaction } from '@solana/web3.js'
export default class EmptyWallet implements Wallet {
constructor(readonly payer: Keypair) {}
async signTransaction(tx: Transaction): Promise<Transaction> {
tx.partialSign(this.payer)
return tx
}
async signAllTransactions(txs: Transaction[]): Promise<Transaction[]> {
return txs.map((t) => {
t.partialSign(this.payer)
return t
})
}
get publicKey(): PublicKey {
return this.payer.publicKey
}
}

2136
yarn.lock

File diff suppressed because it is too large Load Diff