mango v4 sandbox
This commit is contained in:
parent
8a9a787f72
commit
c8898467db
|
@ -0,0 +1,6 @@
|
|||
**/node_modules/*
|
||||
**/out/*
|
||||
**/.next/*
|
||||
**/public/charting_library/*
|
||||
**/public/datafeeds/*
|
||||
**/components/charting_library/*
|
|
@ -1,5 +1,6 @@
|
|||
.serverless/
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
.env
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
.next
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
public
|
||||
components/charting_library
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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">
|
||||
​
|
||||
</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
|
|
@ -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
|
|
@ -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
|
||||
|
|
25
package.json
25
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
175
pages/index.tsx
175
pages/index.tsx
|
@ -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 →</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 →</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 →</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 →</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
content: ['./pages/**/*.tsx', './components/**/*.tsx'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
// ...
|
||||
require('@tailwindcss/forms'),
|
||||
],
|
||||
}
|
|
@ -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
|
||||
// }
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue