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/
|
.serverless/
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
.env
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
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
|
module.exports = nextConfig
|
||||||
|
|
25
package.json
25
package.json
|
@ -9,16 +9,39 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"next": "12.1.5",
|
||||||
|
"process": "^0.11.10",
|
||||||
"react": "18.0.0",
|
"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": {
|
"devDependencies": {
|
||||||
|
"@types/bn.js": "4.11.6",
|
||||||
"@types/node": "17.0.23",
|
"@types/node": "17.0.23",
|
||||||
"@types/react": "18.0.3",
|
"@types/react": "18.0.3",
|
||||||
"@types/react-dom": "18.0.0",
|
"@types/react-dom": "18.0.0",
|
||||||
|
"autoprefixer": "^10.4.4",
|
||||||
"eslint": "8.13.0",
|
"eslint": "8.13.0",
|
||||||
"eslint-config-next": "12.1.5",
|
"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"
|
"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 type { NextPage } from 'next'
|
||||||
import Head from 'next/head'
|
import { MANGO_V4_ID } from '@blockworks-foundation/mango-v4'
|
||||||
import Image from 'next/image'
|
import {
|
||||||
import styles from '../styles/Home.module.css'
|
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 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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className="">
|
||||||
<Head>
|
<ConnectionProvider endpoint={endpoint}>
|
||||||
<title>Create Next App</title>
|
<WalletProvider wallets={wallets} autoConnect>
|
||||||
<meta name="description" content="Generated by create next app" />
|
<WalletModalProvider>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<WalletListener />
|
||||||
</Head>
|
<TopBar />
|
||||||
|
|
||||||
<main className={styles.main}>
|
<div className="">
|
||||||
<h1 className={styles.title}>
|
<div className="my-2 flex text-lg">
|
||||||
Welcome to <a href="https://nextjs.org">Next.js!</a>
|
<div className="mx-auto">Mango V4 Devnet</div>
|
||||||
</h1>
|
</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}>
|
<div className="mx-auto">
|
||||||
Get started by editing{' '}
|
<SerumOrder />
|
||||||
<code className={styles.code}>pages/index.tsx</code>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
|
</div>
|
||||||
<div className={styles.grid}>
|
</div>
|
||||||
<a href="https://nextjs.org/docs" className={styles.card}>
|
</WalletModalProvider>
|
||||||
<h2>Documentation →</h2>
|
</WalletProvider>
|
||||||
<p>Find in-depth information about Next.js features and API.</p>
|
</ConnectionProvider>
|
||||||
</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>
|
</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,
|
@tailwind base;
|
||||||
body {
|
@tailwind components;
|
||||||
padding: 0;
|
@tailwind utilities;
|
||||||
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;
|
|
||||||
}
|
|
|
@ -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