add trade form component
This commit is contained in:
parent
37cf5bae14
commit
f03f1919a4
14
.babelrc
14
.babelrc
|
@ -10,5 +10,15 @@
|
|||
}
|
||||
]
|
||||
],
|
||||
"plugins": ["xwind/babel", "@emotion/babel-plugin"]
|
||||
}
|
||||
"plugins": [
|
||||
"xwind/babel",
|
||||
"@emotion/babel-plugin",
|
||||
[
|
||||
"import",
|
||||
{
|
||||
"libraryName": "antd",
|
||||
"style": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"plugins": ["@typescript-eslint", "react-hooks"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
// Uncomment the following lines to enable eslint-config-prettier
|
||||
// Is not enabled right now to avoid issues with the Next.js repo
|
||||
// "prettier",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"es6": true,
|
||||
|
@ -24,6 +22,8 @@
|
|||
"react/react-in-jsx-scope": 0,
|
||||
"react/display-name": 0,
|
||||
"react/prop-types": 0,
|
||||
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
|
||||
"react-hooks/exhaustive-deps": "warn", // Checks effect dependencies
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
|
|
241
@types/types.tsx
241
@types/types.tsx
|
@ -1,186 +1,201 @@
|
|||
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
|
||||
import Wallet from '@project-serum/sol-wallet-adapter';
|
||||
import { Market, OpenOrders } from '@project-serum/serum';
|
||||
import { Event } from '@project-serum/serum/lib/queue';
|
||||
import { Order } from '@project-serum/serum/lib/market';
|
||||
import { MangoGroup, MarginAccount, MangoClient } from '@blockworks-foundation/mango-client';
|
||||
import { MangoSrmAccount } from '@blockworks-foundation/mango-client/lib/client';
|
||||
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'
|
||||
import Wallet from '@project-serum/sol-wallet-adapter'
|
||||
import { Market, OpenOrders } from '@project-serum/serum'
|
||||
import { Event } from '@project-serum/serum/lib/queue'
|
||||
import { Order } from '@project-serum/serum/lib/market'
|
||||
import {
|
||||
MangoGroup,
|
||||
MarginAccount,
|
||||
MangoClient,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
import { MangoSrmAccount } from '@blockworks-foundation/mango-client/lib/client'
|
||||
|
||||
export interface ConnectionContextValues {
|
||||
endpoint: string;
|
||||
setEndpoint: (newEndpoint: string) => void;
|
||||
connection: Connection;
|
||||
sendConnection: Connection;
|
||||
availableEndpoints: EndpointInfo[];
|
||||
setCustomEndpoints: (newCustomEndpoints: EndpointInfo[]) => void;
|
||||
endpoint: string
|
||||
setEndpoint: (newEndpoint: string) => void
|
||||
connection: Connection
|
||||
sendConnection: Connection
|
||||
availableEndpoints: EndpointInfo[]
|
||||
setCustomEndpoints: (newCustomEndpoints: EndpointInfo[]) => void
|
||||
}
|
||||
|
||||
export interface WalletContextValues {
|
||||
wallet: Wallet;
|
||||
connected: boolean;
|
||||
providerUrl: string;
|
||||
setProviderUrl: (newProviderUrl: string) => void;
|
||||
providerName: string;
|
||||
wallet: Wallet
|
||||
connected: boolean
|
||||
providerUrl: string
|
||||
setProviderUrl: (newProviderUrl: string) => void
|
||||
providerName: string
|
||||
}
|
||||
|
||||
export interface MarketInfo {
|
||||
address: PublicKey;
|
||||
name: string;
|
||||
programId: PublicKey;
|
||||
deprecated: boolean;
|
||||
quoteLabel?: string;
|
||||
baseLabel?: string;
|
||||
address: PublicKey
|
||||
name: string
|
||||
programId: PublicKey
|
||||
deprecated: boolean
|
||||
quoteLabel?: string
|
||||
baseLabel?: string
|
||||
}
|
||||
|
||||
export interface CustomMarketInfo {
|
||||
address: string;
|
||||
name: string;
|
||||
programId: string;
|
||||
quoteLabel?: string;
|
||||
baseLabel?: string;
|
||||
address: string
|
||||
name: string
|
||||
programId: string
|
||||
quoteLabel?: string
|
||||
baseLabel?: string
|
||||
}
|
||||
|
||||
export interface FullMarketInfo {
|
||||
address?: PublicKey;
|
||||
name?: string;
|
||||
programId?: PublicKey;
|
||||
deprecated?: boolean;
|
||||
quoteLabel?: string;
|
||||
baseLabel?: string;
|
||||
marketName?: string;
|
||||
baseCurrency?: string;
|
||||
quoteCurrency?: string;
|
||||
marketInfo?: MarketInfo;
|
||||
address?: PublicKey
|
||||
name?: string
|
||||
programId?: PublicKey
|
||||
deprecated?: boolean
|
||||
quoteLabel?: string
|
||||
baseLabel?: string
|
||||
marketName?: string
|
||||
baseCurrency?: string
|
||||
quoteCurrency?: string
|
||||
marketInfo?: MarketInfo
|
||||
}
|
||||
|
||||
export interface MarketContextValues extends FullMarketInfo {
|
||||
market: Market | undefined | null;
|
||||
setMarketAddress: (newMarketAddress: string) => void;
|
||||
market: Market | undefined | null
|
||||
setMarketAddress: (newMarketAddress: string) => void
|
||||
}
|
||||
|
||||
export interface TokenAccount {
|
||||
pubkey: PublicKey;
|
||||
account: AccountInfo<Buffer> | null;
|
||||
effectiveMint: PublicKey;
|
||||
pubkey: PublicKey
|
||||
account: AccountInfo<Buffer> | null
|
||||
effectiveMint: PublicKey
|
||||
}
|
||||
|
||||
export interface Trade extends Event {
|
||||
side: string;
|
||||
price: number;
|
||||
feeCost: number;
|
||||
size: number;
|
||||
side: string
|
||||
price: number
|
||||
feeCost: number
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface OrderWithMarket extends Order {
|
||||
marketName: string;
|
||||
marketName: string
|
||||
}
|
||||
|
||||
export interface OrderWithMarketAndMarketName extends Order {
|
||||
market: Market;
|
||||
marketName: string | undefined;
|
||||
market: Market
|
||||
marketName: string | undefined
|
||||
}
|
||||
|
||||
interface BalancesBase {
|
||||
key: string;
|
||||
coin: string;
|
||||
wallet?: number | null | undefined;
|
||||
orders?: number | null | undefined;
|
||||
openOrders?: OpenOrders | null | undefined;
|
||||
unsettled?: number | null | undefined;
|
||||
key: string
|
||||
coin: string
|
||||
wallet?: number | null | undefined
|
||||
orders?: number | null | undefined
|
||||
openOrders?: OpenOrders | null | undefined
|
||||
unsettled?: number | null | undefined
|
||||
}
|
||||
|
||||
export interface Balances extends BalancesBase {
|
||||
market?: Market | null | undefined;
|
||||
marginDeposits?: number | null | undefined;
|
||||
borrows?: number | null | undefined;
|
||||
net?: number | null | undefined;
|
||||
market?: Market | null | undefined
|
||||
marginDeposits?: number | null | undefined
|
||||
borrows?: number | null | undefined
|
||||
net?: number | null | undefined
|
||||
}
|
||||
|
||||
export interface OpenOrdersBalances extends BalancesBase {
|
||||
market?: string | null | undefined;
|
||||
baseCurrencyAccount: { pubkey: PublicKey; account: AccountInfo<Buffer> } | null | undefined;
|
||||
quoteCurrencyAccount: { pubkey: PublicKey; account: AccountInfo<Buffer> } | null | undefined;
|
||||
market?: string | null | undefined
|
||||
baseCurrencyAccount:
|
||||
| { pubkey: PublicKey; account: AccountInfo<Buffer> }
|
||||
| null
|
||||
| undefined
|
||||
quoteCurrencyAccount:
|
||||
| { pubkey: PublicKey; account: AccountInfo<Buffer> }
|
||||
| null
|
||||
| undefined
|
||||
}
|
||||
|
||||
export interface DeprecatedOpenOrdersBalances extends BalancesBase {
|
||||
market: Market | null | undefined;
|
||||
marketName: string | null | undefined;
|
||||
market: Market | null | undefined
|
||||
marketName: string | null | undefined
|
||||
}
|
||||
|
||||
export interface PreferencesContextValues {
|
||||
autoSettleEnabled: boolean;
|
||||
setAutoSettleEnabled: (newAutoSettleEnabled: boolean) => void;
|
||||
autoSettleEnabled: boolean
|
||||
setAutoSettleEnabled: (newAutoSettleEnabled: boolean) => void
|
||||
}
|
||||
|
||||
export interface EndpointInfo {
|
||||
name: string;
|
||||
endpoint: string;
|
||||
custom: boolean;
|
||||
name: string
|
||||
endpoint: string
|
||||
custom: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* {tokenMint: preferred token account's base58 encoded public key}
|
||||
*/
|
||||
export interface SelectedTokenAccounts {
|
||||
[tokenMint: string]: string;
|
||||
[tokenMint: string]: string
|
||||
}
|
||||
|
||||
export interface BonfidaTrade {
|
||||
market: string;
|
||||
size: number;
|
||||
price: number;
|
||||
orderId: string;
|
||||
time: number;
|
||||
side: string;
|
||||
feeCost: number;
|
||||
marketAddress: string;
|
||||
export interface ChartType {
|
||||
market: string
|
||||
size: number
|
||||
price: number
|
||||
orderId: string
|
||||
time: number
|
||||
side: string
|
||||
feeCost: number
|
||||
marketAddress: string
|
||||
}
|
||||
|
||||
export interface FeeRates {
|
||||
taker: number;
|
||||
maker: number;
|
||||
taker: number
|
||||
maker: number
|
||||
}
|
||||
|
||||
export interface SwapContextValues {
|
||||
slippage: number;
|
||||
setSlippage: (newSlippage: number) => void;
|
||||
tokenProgramId: PublicKey;
|
||||
swapProgramId: PublicKey;
|
||||
legacySwapProgramIds: PublicKey[];
|
||||
programIds: () => { token: PublicKey; swap: PublicKey };
|
||||
slippage: number
|
||||
setSlippage: (newSlippage: number) => void
|
||||
tokenProgramId: PublicKey
|
||||
swapProgramId: PublicKey
|
||||
legacySwapProgramIds: PublicKey[]
|
||||
programIds: () => { token: PublicKey; swap: PublicKey }
|
||||
}
|
||||
|
||||
// Margin Account Type declaration
|
||||
export interface MarginAccountContextValues {
|
||||
marginAccount: MarginAccount | null; // The current margin account trading with
|
||||
marginAccounts: MarginAccount[] | []; // List of all margin account pk in a mango group
|
||||
mango_groups: Array<string>; // Identifier for the mango group
|
||||
mangoOptions: any; //The different parameters for our mango program
|
||||
mangoClient: MangoClient; // Instance of mango clinet
|
||||
mangoGroup: MangoGroup | null; // The current mango group
|
||||
setMarginAccount: (marginAccount: null | MarginAccount) => void;
|
||||
setMarginAccounts: (marginAccounts: MarginAccount[]) => void;
|
||||
createMarginAccount: () => Promise<MarginAccount | null>; // For creating a margin account
|
||||
maPending: any; // Is the context updating
|
||||
setMAPending: (any) => void; // Set the pending states on margin account transactions
|
||||
getMarginAccount: (pubKey: PublicKey | undefined) => Promise<MarginAccount | null>;
|
||||
size: { currency: string; size: number }; // The size of buy or sell on tradeform
|
||||
setSize: (size: { currency: string; size: number }) => void; // Set the size on trade form
|
||||
srmFeeRates: FeeRates | null;
|
||||
totalSrm: number;
|
||||
contributedSrm: number;
|
||||
mangoSrmAccounts: MangoSrmAccount[] | null;
|
||||
getUserSrmInfo: () => void;
|
||||
marginAccount: MarginAccount | null // The current margin account trading with
|
||||
marginAccounts: MarginAccount[] | [] // List of all margin account pk in a mango group
|
||||
mango_groups: Array<string> // Identifier for the mango group
|
||||
mangoOptions: any //The different parameters for our mango program
|
||||
mangoClient: MangoClient // Instance of mango clinet
|
||||
mangoGroup: MangoGroup | null // The current mango group
|
||||
setMarginAccount: (marginAccount: null | MarginAccount) => void
|
||||
setMarginAccounts: (marginAccounts: MarginAccount[]) => void
|
||||
createMarginAccount: () => Promise<MarginAccount | null> // For creating a margin account
|
||||
maPending: any // Is the context updating
|
||||
setMAPending: (any) => void // Set the pending states on margin account transactions
|
||||
getMarginAccount: (
|
||||
pubKey: PublicKey | undefined
|
||||
) => Promise<MarginAccount | null>
|
||||
size: { currency: string; size: number } // The size of buy or sell on tradeform
|
||||
setSize: (size: { currency: string; size: number }) => void // Set the size on trade form
|
||||
srmFeeRates: FeeRates | null
|
||||
totalSrm: number
|
||||
contributedSrm: number
|
||||
mangoSrmAccounts: MangoSrmAccount[] | null
|
||||
getUserSrmInfo: () => void
|
||||
}
|
||||
|
||||
// Type declaration for the margin accounts for the mango group
|
||||
|
||||
export type mangoTokenAccounts = { mango_group: string; accounts: TokenAccount[] };
|
||||
export type mangoTokenAccounts = {
|
||||
mango_group: string
|
||||
accounts: TokenAccount[]
|
||||
}
|
||||
|
||||
// Token infos
|
||||
export interface KnownToken {
|
||||
tokenSymbol: string;
|
||||
tokenName: string;
|
||||
icon?: string;
|
||||
mintAddress: string;
|
||||
tokenSymbol: string
|
||||
tokenName: string
|
||||
icon?: string
|
||||
mintAddress: string
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@ import xw from 'xwind'
|
|||
import useInterval from '../hooks/useInterval'
|
||||
import usePrevious from '../hooks/usePrevious'
|
||||
import { isEqual, getDecimalCount } from '../utils/'
|
||||
// import { useMarket, useOrderbook, useMarkPrice } from '../utils/markets'
|
||||
// import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
|
||||
import { ArrowUpIcon, ArrowDownIcon } from '@heroicons/react/solid'
|
||||
import useMarkPrice from '../hooks/useMarkPrice'
|
||||
import useOrderbook from '../hooks/useOrderbook'
|
||||
import useMarkets from '../hooks/useMarkets'
|
||||
|
||||
const Title = styled.div`
|
||||
color: rgba(255, 255, 255, 1);
|
||||
|
@ -16,11 +18,6 @@ const SizeTitle = styled.div`
|
|||
color: #434a59;
|
||||
`
|
||||
|
||||
const MarkPriceTitle = styled.div`
|
||||
padding: 4px 0 4px;
|
||||
font-weight: 700;
|
||||
`
|
||||
|
||||
const Line = styled.div<any>`
|
||||
text-align: ${(props) => (props.invert ? 'left' : 'right')};
|
||||
float: ${(props) => (props.invert ? 'left' : 'right')};
|
||||
|
@ -41,7 +38,7 @@ export default function Orderbook({ depth = 7 }) {
|
|||
const smallScreen = false
|
||||
const markPrice = useMarkPrice()
|
||||
const [orderbook] = useOrderbook()
|
||||
const { baseCurrency, quoteCurrency } = useMarket()
|
||||
const { baseCurrency, quoteCurrency } = useMarkets()
|
||||
|
||||
const currentOrderbookData = useRef(null)
|
||||
const lastOrderbookData = useRef(null)
|
||||
|
@ -99,116 +96,108 @@ export default function Orderbook({ depth = 7 }) {
|
|||
return cumulative
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <div>
|
||||
// <Title>Orderbook</Title>
|
||||
// </div>
|
||||
// {smallScreen ? (
|
||||
// <>
|
||||
// <MarkPriceComponent markPrice={markPrice} />
|
||||
// <div css={xw`flex`}>
|
||||
// <div css={xw`flex`} flex={1}>
|
||||
// <SizeTitle>
|
||||
// <div css={xw`text-left`}>
|
||||
// Size
|
||||
// <div style={{ marginTop: -5 }}>({baseCurrency})</div>
|
||||
// </div>
|
||||
// <div css={xw`text-right pr-4`}>
|
||||
// Price
|
||||
// <div style={{ marginTop: -5 }}>({quoteCurrency})</div>
|
||||
// </div>
|
||||
// </SizeTitle>
|
||||
// {orderbookData?.bids.map(({ price, size, sizePercent }) => (
|
||||
// <OrderbookRow
|
||||
// key={price + ''}
|
||||
// price={price}
|
||||
// size={size}
|
||||
// side={'buy'}
|
||||
// sizePercent={sizePercent}
|
||||
// // onPriceClick={() => onPrice(price)}
|
||||
// // onSizeClick={() => onSize(size)}
|
||||
// />
|
||||
// ))}
|
||||
// </div>
|
||||
// <Col flex={1} style={{ paddingLeft: 2 }}>
|
||||
// <SizeTitle>
|
||||
// <Col span={12} style={{ textAlign: 'left' }}>
|
||||
// Price
|
||||
// <div style={{ marginTop: -5 }}>({quoteCurrency})</div>
|
||||
// </Col>
|
||||
// <Col span={12} style={{ textAlign: 'right ' }}>
|
||||
// Size
|
||||
// <div style={{ marginTop: -5 }}>({baseCurrency})</div>
|
||||
// </Col>
|
||||
// </SizeTitle>
|
||||
// {orderbookData?.asks
|
||||
// .slice(0)
|
||||
// .reverse()
|
||||
// .map(({ price, size, sizePercent }) => (
|
||||
// <OrderbookRow
|
||||
// invert={true}
|
||||
// key={price + ''}
|
||||
// price={price}
|
||||
// size={size}
|
||||
// side={'sell'}
|
||||
// sizePercent={sizePercent}
|
||||
// onPriceClick={() => onPrice(price)}
|
||||
// onSizeClick={() => onSize(size)}
|
||||
// />
|
||||
// ))}
|
||||
// </Col>
|
||||
// </div>
|
||||
// </>
|
||||
// ) : (
|
||||
// <>
|
||||
// <SizeTitle>
|
||||
// <Col span={12} style={{ textAlign: 'left' }}>
|
||||
// Size ({baseCurrency})
|
||||
// </Col>
|
||||
// <Col span={12} style={{ textAlign: 'right' }}>
|
||||
// Price ({quoteCurrency})
|
||||
// </Col>
|
||||
// </SizeTitle>
|
||||
// {orderbookData?.asks.map(({ price, size, sizePercent }) => (
|
||||
// <OrderbookRow
|
||||
// key={price + ''}
|
||||
// price={price}
|
||||
// size={size}
|
||||
// side={'sell'}
|
||||
// sizePercent={sizePercent}
|
||||
// onPriceClick={() => onPrice(price)}
|
||||
// onSizeClick={() => onSize(size)}
|
||||
// />
|
||||
// ))}
|
||||
// <MarkPriceComponent markPrice={markPrice} />
|
||||
// {orderbookData?.bids.map(({ price, size, sizePercent }) => (
|
||||
// <OrderbookRow
|
||||
// key={price + ''}
|
||||
// price={price}
|
||||
// size={size}
|
||||
// side={'buy'}
|
||||
// sizePercent={sizePercent}
|
||||
// onPriceClick={() => onPrice(price)}
|
||||
// onSizeClick={() => onSize(size)}
|
||||
// />
|
||||
// ))}
|
||||
// </>
|
||||
// )}
|
||||
// </>
|
||||
// )
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Title>Orderbook</Title>
|
||||
</div>
|
||||
{smallScreen ? (
|
||||
<>
|
||||
<MarkPriceComponent markPrice={markPrice} />
|
||||
<div css={xw`flex`}>
|
||||
<div css={xw`flex`}>
|
||||
<SizeTitle>
|
||||
<div css={xw`text-left`}>
|
||||
Size
|
||||
<div style={{ marginTop: -5 }}>({baseCurrency})</div>
|
||||
</div>
|
||||
<div css={xw`text-right pr-4`}>
|
||||
Price
|
||||
<div style={{ marginTop: -5 }}>({quoteCurrency})</div>
|
||||
</div>
|
||||
</SizeTitle>
|
||||
{orderbookData?.bids.map(({ price, size, sizePercent }) => (
|
||||
<OrderbookRow
|
||||
key={price + ''}
|
||||
price={price}
|
||||
size={size}
|
||||
side={'buy'}
|
||||
sizePercent={sizePercent}
|
||||
// onPriceClick={() => onPrice(price)}
|
||||
// onSizeClick={() => onSize(size)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div css={xw`flex pl-1`}>
|
||||
<SizeTitle>
|
||||
<div css={xw`text-left`}>
|
||||
Price
|
||||
<div style={{ marginTop: -5 }}>({quoteCurrency})</div>
|
||||
</div>
|
||||
<div css={xw`text-right`}>
|
||||
Size
|
||||
<div style={{ marginTop: -5 }}>({baseCurrency})</div>
|
||||
</div>
|
||||
</SizeTitle>
|
||||
{orderbookData?.asks
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map(({ price, size, sizePercent }) => (
|
||||
<OrderbookRow
|
||||
invert
|
||||
key={price + ''}
|
||||
price={price}
|
||||
size={size}
|
||||
side={'sell'}
|
||||
sizePercent={sizePercent}
|
||||
onPriceClick={() => alert(`price ${price}`)}
|
||||
onSizeClick={() => alert(`size ${size}`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SizeTitle>
|
||||
<div style={{ textAlign: 'left' }}>Size ({baseCurrency})</div>
|
||||
<div style={{ textAlign: 'right' }}>Price ({quoteCurrency})</div>
|
||||
</SizeTitle>
|
||||
{orderbookData?.asks.map(({ price, size, sizePercent }) => (
|
||||
<OrderbookRow
|
||||
key={price + ''}
|
||||
price={price}
|
||||
size={size}
|
||||
side={'sell'}
|
||||
sizePercent={sizePercent}
|
||||
onPriceClick={() => alert(`price ${price}`)}
|
||||
onSizeClick={() => alert(`size ${size}`)}
|
||||
/>
|
||||
))}
|
||||
<MarkPriceComponent markPrice={markPrice} />
|
||||
{orderbookData?.bids.map(({ price, size, sizePercent }) => (
|
||||
<OrderbookRow
|
||||
key={price + ''}
|
||||
price={price}
|
||||
size={size}
|
||||
side={'buy'}
|
||||
sizePercent={sizePercent}
|
||||
onPriceClick={() => alert(`price ${price}`)}
|
||||
onSizeClick={() => alert(`size ${size}`)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const OrderbookRow = React.memo(
|
||||
const OrderbookRow = React.memo<any>(
|
||||
({ side, price, size, sizePercent, onSizeClick, onPriceClick, invert }) => {
|
||||
const element = useRef()
|
||||
|
||||
const { market } = useMarket()
|
||||
const element = useRef(null)
|
||||
const { market } = useMarkets()
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line
|
||||
!element.current?.classList.contains('flash') &&
|
||||
element.current?.classList.add('flash')
|
||||
const id = setTimeout(
|
||||
|
@ -257,10 +246,8 @@ const OrderbookRow = React.memo(
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<Col span={12} style={{ textAlign: 'left' }}>
|
||||
{formattedSize}
|
||||
</Col>
|
||||
<Col span={12} style={{ textAlign: 'right' }}>
|
||||
<div css={xw`text-left`}>{formattedSize}</div>
|
||||
<div css={xw`text-right`}>
|
||||
<Line
|
||||
data-width={sizePercent + '%'}
|
||||
data-bgcolor={side === 'buy' ? '#5b6b16' : '#E54033'}
|
||||
|
@ -271,7 +258,7 @@ const OrderbookRow = React.memo(
|
|||
>
|
||||
{formattedPrice}
|
||||
</Price>
|
||||
</Col>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -281,10 +268,10 @@ const OrderbookRow = React.memo(
|
|||
isEqual(prevProps, nextProps, ['price', 'size', 'sizePercent'])
|
||||
)
|
||||
|
||||
const MarkPriceComponent = React.memo(
|
||||
const MarkPriceComponent = React.memo<{ markPrice: number }>(
|
||||
({ markPrice }) => {
|
||||
const { market } = useMarket()
|
||||
const previousMarkPrice = usePrevious(markPrice)
|
||||
const { market } = useMarkets()
|
||||
const previousMarkPrice: number = usePrevious(markPrice)
|
||||
|
||||
const markPriceColor =
|
||||
markPrice > previousMarkPrice
|
||||
|
@ -299,17 +286,18 @@ const MarkPriceComponent = React.memo(
|
|||
markPrice.toFixed(getDecimalCount(market.tickSize))
|
||||
|
||||
return (
|
||||
<MarkPriceTitle justify="center">
|
||||
<div style={{ color: markPriceColor }}>
|
||||
{markPrice > previousMarkPrice && (
|
||||
<ArrowUpOutlined style={{ marginRight: 5 }} />
|
||||
)}
|
||||
{markPrice < previousMarkPrice && (
|
||||
<ArrowDownOutlined style={{ marginRight: 5 }} />
|
||||
)}
|
||||
{formattedMarkPrice || '----'}
|
||||
</div>
|
||||
</MarkPriceTitle>
|
||||
<div
|
||||
css={xw`flex justify-center items-center font-bold p-1`}
|
||||
style={{ color: markPriceColor }}
|
||||
>
|
||||
{markPrice > previousMarkPrice && (
|
||||
<ArrowUpIcon css={xw`h-5 w-5 mr-1`} />
|
||||
)}
|
||||
{markPrice < previousMarkPrice && (
|
||||
<ArrowDownIcon css={xw`h-5 w-5 mr-1`} />
|
||||
)}
|
||||
{formattedMarkPrice || '----'}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => isEqual(prevProps, nextProps, ['markPrice'])
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import { useState } from 'react'
|
||||
import xw from 'xwind'
|
||||
import MenuItem from './MenuItem'
|
||||
import useStore from '../hooks/useStore'
|
||||
import { useWallet } from '../hooks/useWallet'
|
||||
import useWallet from '../hooks/useWallet'
|
||||
|
||||
const TopBar = () => {
|
||||
const connected = useStore((state) => state.wallet.connected)
|
||||
const wallet = useWallet()
|
||||
console.log('load topbar')
|
||||
|
||||
const { connected, wallet } = useWallet()
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
|
||||
const handleConnectDisconnect = () =>
|
||||
const handleConnectDisconnect = () => {
|
||||
connected ? wallet.disconnect() : wallet.connect()
|
||||
}
|
||||
|
||||
return (
|
||||
<nav css={xw`bg-mango-dark`}>
|
||||
|
|
|
@ -0,0 +1,404 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Button, Input, Radio, Switch, Select } from 'antd'
|
||||
import styled from '@emotion/styled'
|
||||
import useMarkets from '../hooks/useMarkets'
|
||||
import useWallet from '../hooks/useWallet'
|
||||
import useMarkPrice from '../hooks/useMarkPrice'
|
||||
import useOrderbook from '../hooks/useOrderbook'
|
||||
import useIpAddress from '../hooks/useIpAddress'
|
||||
import useConnection from '../hooks/useConnection'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { IDS } from '@blockworks-foundation/mango-client'
|
||||
import { notify } from '../utils/notifications'
|
||||
import { placeAndSettle } from '../utils/mango'
|
||||
import { calculateMarketPrice, getDecimalCount } from '../utils'
|
||||
import FloatingElement from './FloatingElement'
|
||||
import { roundToDecimal } from '../utils/index'
|
||||
import useMarginAccount from '../hooks/useMarginAcccount'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
|
||||
const SellButton = styled(Button)`
|
||||
margin: 20px 0px 0px 0px;
|
||||
background: #e54033;
|
||||
border-color: #e54033;
|
||||
`
|
||||
|
||||
const BuyButton = styled(Button)`
|
||||
margin: 20px 0px 0px 0px;
|
||||
color: #141026;
|
||||
background: #9bd104;
|
||||
border-color: #9bd104;
|
||||
`
|
||||
|
||||
export default function TradeForm({
|
||||
setChangeOrderRef,
|
||||
}: {
|
||||
setChangeOrderRef?: (
|
||||
ref: ({ size, price }: { size?: number; price?: number }) => void
|
||||
) => void
|
||||
}) {
|
||||
const [side, setSide] = useState<'buy' | 'sell'>('buy')
|
||||
const { baseCurrency, quoteCurrency, market } = useMarkets()
|
||||
const address = market?.publicKey
|
||||
const { wallet, connected } = useWallet()
|
||||
|
||||
const { connection, cluster } = useConnection()
|
||||
const { marginAccount, mangoGroup, tradeForm } = useMarginAccount()
|
||||
console.log('margin accoun hook', { marginAccount, mangoGroup, tradeForm })
|
||||
|
||||
const orderBookRef = useRef(useMangoStore.getState().market.orderBook)
|
||||
const orderbook = orderBookRef.current[0]
|
||||
useEffect(
|
||||
() =>
|
||||
useMangoStore.subscribe(
|
||||
(orderBook) => (orderBookRef.current = orderBook as any[]),
|
||||
(state) => state.market.orderBook
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const markPriceRef = useRef(useMangoStore.getState().market.markPrice)
|
||||
const markPrice = markPriceRef.current
|
||||
useEffect(
|
||||
() =>
|
||||
useMangoStore.subscribe(
|
||||
(markPrice) => (markPriceRef.current = markPrice as number),
|
||||
(state) => state.market.markPrice
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const { ipAllowed } = useIpAddress()
|
||||
|
||||
const [postOnly, setPostOnly] = useState(false)
|
||||
const [ioc, setIoc] = useState(false)
|
||||
const [baseSize, setBaseSize] = useState<number | undefined>(undefined)
|
||||
const [quoteSize, setQuoteSize] = useState<number | undefined>(undefined)
|
||||
const [price, setPrice] = useState<number | undefined>(undefined)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [tradeType, setTradeType] = useState('Limit')
|
||||
|
||||
const sizeDecimalCount =
|
||||
market?.minOrderSize && getDecimalCount(market.minOrderSize)
|
||||
const priceDecimalCount = market?.tickSize && getDecimalCount(market.tickSize)
|
||||
|
||||
useEffect(() => {
|
||||
setChangeOrderRef && setChangeOrderRef(doChangeOrder)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setChangeOrderRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (!price && markPrice && tradeType !== 'Market') {
|
||||
setPrice(markPrice)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [price, baseSize, quoteSize])
|
||||
|
||||
// Set the price from the balance comp
|
||||
useEffect(() => {
|
||||
if (tradeForm.currency) {
|
||||
if (tradeForm.currency === baseCurrency) {
|
||||
// onSetBaseSize(size.size);
|
||||
} else {
|
||||
// onSetQuoteSize(size.size);
|
||||
}
|
||||
}
|
||||
}, [tradeForm])
|
||||
|
||||
const onSetBaseSize = (baseSize: number | undefined) => {
|
||||
setBaseSize(baseSize)
|
||||
if (!baseSize) {
|
||||
setQuoteSize(undefined)
|
||||
return
|
||||
}
|
||||
const usePrice = price || markPrice
|
||||
if (!usePrice) {
|
||||
setQuoteSize(undefined)
|
||||
return
|
||||
}
|
||||
const rawQuoteSize = baseSize * usePrice
|
||||
const quoteSize = baseSize && roundToDecimal(rawQuoteSize, sizeDecimalCount)
|
||||
setQuoteSize(quoteSize)
|
||||
}
|
||||
|
||||
const onSetQuoteSize = (quoteSize: number | undefined) => {
|
||||
setQuoteSize(quoteSize)
|
||||
if (!quoteSize) {
|
||||
setBaseSize(undefined)
|
||||
return
|
||||
}
|
||||
const usePrice = price || markPrice
|
||||
if (!usePrice) {
|
||||
setBaseSize(undefined)
|
||||
return
|
||||
}
|
||||
const rawBaseSize = quoteSize / usePrice
|
||||
const baseSize = quoteSize && roundToDecimal(rawBaseSize, sizeDecimalCount)
|
||||
setBaseSize(baseSize)
|
||||
}
|
||||
|
||||
const doChangeOrder = ({
|
||||
size,
|
||||
price,
|
||||
}: {
|
||||
size?: number
|
||||
price?: number
|
||||
}) => {
|
||||
const formattedSize = size && roundToDecimal(size, sizeDecimalCount)
|
||||
const formattedPrice = price && roundToDecimal(price, priceDecimalCount)
|
||||
formattedSize && onSetBaseSize(formattedSize)
|
||||
formattedPrice && setPrice(formattedPrice)
|
||||
}
|
||||
|
||||
const postOnChange = (checked) => {
|
||||
if (checked) {
|
||||
setIoc(false)
|
||||
}
|
||||
setPostOnly(checked)
|
||||
}
|
||||
const iocOnChange = (checked) => {
|
||||
if (checked) {
|
||||
setPostOnly(false)
|
||||
}
|
||||
setIoc(checked)
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!price && tradeType === 'Limit') {
|
||||
console.warn('Missing price')
|
||||
notify({
|
||||
message: 'Missing price',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
} else if (!baseSize) {
|
||||
console.warn('Missing size')
|
||||
notify({
|
||||
message: 'Missing size',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
console.log('checking if we can call place order', {
|
||||
mangoGroup,
|
||||
address,
|
||||
marginAccount,
|
||||
market,
|
||||
})
|
||||
|
||||
if (!mangoGroup || !address || !marginAccount || !market) return
|
||||
setSubmitting(true)
|
||||
|
||||
try {
|
||||
let calculatedPrice
|
||||
if (tradeType === 'Market') {
|
||||
calculatedPrice =
|
||||
side === 'buy'
|
||||
? calculateMarketPrice(orderbook.asks, tradeForm.size, side)
|
||||
: calculateMarketPrice(orderbook.bids, tradeForm.size, side)
|
||||
}
|
||||
|
||||
await placeAndSettle(
|
||||
connection,
|
||||
new PublicKey(IDS[cluster].mango_program_id),
|
||||
mangoGroup,
|
||||
marginAccount,
|
||||
market,
|
||||
wallet,
|
||||
side,
|
||||
calculatedPrice ?? price,
|
||||
baseSize,
|
||||
ioc ? 'ioc' : postOnly ? 'postOnly' : 'limit'
|
||||
)
|
||||
|
||||
// refreshCache(tuple('getTokenAccounts', wallet, connected))
|
||||
setPrice(undefined)
|
||||
onSetBaseSize(undefined)
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
notify({
|
||||
message: 'Error placing order',
|
||||
description: e.message,
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTradeTypeChange = (tradeType) => {
|
||||
setTradeType(tradeType)
|
||||
if (tradeType === 'Market') {
|
||||
setIoc(true)
|
||||
setPrice(undefined)
|
||||
} else {
|
||||
const limitPrice =
|
||||
side === 'buy' ? orderbook.asks[0][0] : orderbook.bids[0][0]
|
||||
setPrice(limitPrice)
|
||||
setIoc(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FloatingElement>
|
||||
<div>
|
||||
<Radio.Group
|
||||
onChange={(e) => setSide(e.target.value)}
|
||||
value={side}
|
||||
buttonStyle="solid"
|
||||
style={{
|
||||
marginBottom: 8,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Radio.Button
|
||||
value="buy"
|
||||
style={{
|
||||
width: '50%',
|
||||
textAlign: 'center',
|
||||
color: side === 'buy' ? '#141026' : '',
|
||||
background: side === 'buy' ? '#AFD803' : '',
|
||||
borderColor: side === 'buy' ? '#AFD803' : '',
|
||||
}}
|
||||
>
|
||||
BUY
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className="sell-button"
|
||||
value="sell"
|
||||
style={{
|
||||
width: '50%',
|
||||
textAlign: 'center',
|
||||
background: side === 'sell' ? '#E54033' : '',
|
||||
borderColor: side === 'sell' ? '#E54033' : '',
|
||||
}}
|
||||
>
|
||||
SELL
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Input.Group compact style={{ paddingBottom: 8 }}>
|
||||
<Input
|
||||
style={{
|
||||
width: 'calc(50% + 30px)',
|
||||
textAlign: 'right',
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
addonBefore={<div style={{ width: '30px' }}>Price</div>}
|
||||
suffix={
|
||||
<span style={{ fontSize: 10, opacity: 0.5 }}>
|
||||
{quoteCurrency}
|
||||
</span>
|
||||
}
|
||||
value={price}
|
||||
type="number"
|
||||
step={market?.tickSize || 1}
|
||||
onChange={(e) => setPrice(parseFloat(e.target.value))}
|
||||
disabled={tradeType === 'Market'}
|
||||
/>
|
||||
<Select
|
||||
style={{ width: 'calc(50% - 30px)' }}
|
||||
onChange={handleTradeTypeChange}
|
||||
value={tradeType}
|
||||
>
|
||||
<Select.Option value="Limit">Limit</Select.Option>
|
||||
<Select.Option value="Market">Market</Select.Option>
|
||||
</Select>
|
||||
</Input.Group>
|
||||
<Input.Group compact style={{ paddingBottom: 8 }}>
|
||||
<Input
|
||||
style={{ width: 'calc(50% + 30px)', textAlign: 'right' }}
|
||||
addonBefore={<div style={{ width: '30px' }}>Size</div>}
|
||||
suffix={
|
||||
<span style={{ fontSize: 10, opacity: 0.5 }}>{baseCurrency}</span>
|
||||
}
|
||||
value={baseSize}
|
||||
type="number"
|
||||
step={market?.minOrderSize || 1}
|
||||
onChange={(e) => onSetBaseSize(parseFloat(e.target.value))}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: 'calc(50% - 30px)', textAlign: 'right' }}
|
||||
suffix={
|
||||
<span style={{ fontSize: 10, opacity: 0.5 }}>
|
||||
{quoteCurrency}
|
||||
</span>
|
||||
}
|
||||
value={quoteSize}
|
||||
type="number"
|
||||
step={market?.minOrderSize || 1}
|
||||
onChange={(e) => onSetQuoteSize(parseFloat(e.target.value))}
|
||||
/>
|
||||
</Input.Group>
|
||||
{/* {connected && marginInfo.prices.length ? (
|
||||
<StyledSlider
|
||||
value={sizeFraction}
|
||||
onChange={onSliderChange}
|
||||
min={getSliderMin()}
|
||||
max={getSliderMax()}
|
||||
step={getSliderStep()}
|
||||
tooltipVisible={false}
|
||||
renderTrack={Track}
|
||||
renderThumb={Thumb}
|
||||
/>
|
||||
) : null} */}
|
||||
{tradeType !== 'Market' ? (
|
||||
<div style={{ paddingTop: 18 }}>
|
||||
{'POST '}
|
||||
<Switch
|
||||
checked={postOnly}
|
||||
onChange={postOnChange}
|
||||
style={{ marginRight: 40 }}
|
||||
disabled={tradeType === 'Market'}
|
||||
/>
|
||||
{'IOC '}
|
||||
<Switch
|
||||
checked={ioc}
|
||||
onChange={iocOnChange}
|
||||
disabled={tradeType === 'Market'}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{ipAllowed ? (
|
||||
side === 'buy' ? (
|
||||
<BuyButton
|
||||
disabled={
|
||||
(!price && tradeType === 'Limit') || !baseSize || !connected
|
||||
}
|
||||
onClick={onSubmit}
|
||||
block
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={submitting}
|
||||
>
|
||||
{connected ? `Buy ${baseCurrency}` : 'CONNECT WALLET TO TRADE'}
|
||||
</BuyButton>
|
||||
) : (
|
||||
<SellButton
|
||||
disabled={
|
||||
(!price && tradeType === 'Limit') || !baseSize || !connected
|
||||
}
|
||||
onClick={onSubmit}
|
||||
block
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={submitting}
|
||||
>
|
||||
{connected ? `Sell ${baseCurrency}` : 'CONNECT WALLET TO TRADE'}
|
||||
</SellButton>
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
size="large"
|
||||
style={{
|
||||
margin: '20px 0px 0px 0px',
|
||||
}}
|
||||
disabled
|
||||
>
|
||||
Country Not Allowed
|
||||
</Button>
|
||||
)}
|
||||
</FloatingElement>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// import xw from 'xwind'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Responsive, WidthProvider } from 'react-grid-layout'
|
||||
|
||||
const TVChartContainer = dynamic(
|
||||
() => import('../components/TradingView/index'),
|
||||
{ ssr: false }
|
||||
)
|
||||
import FloatingElement from '../components/FloatingElement'
|
||||
import Orderbook from '../components/Orderbook'
|
||||
import TradeForm from './TradeForm'
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive)
|
||||
|
||||
const TradePageGrid = () => {
|
||||
const layouts = {
|
||||
lg: [
|
||||
{ i: '1', x: 0, y: 0, w: 2, h: 2 },
|
||||
{ i: '2', x: 2, y: 0, w: 1, h: 2 },
|
||||
{ i: '3', x: 3, y: 0, w: 1, h: 1 },
|
||||
{ i: '4', x: 3, y: 1, w: 1, h: 1 },
|
||||
{ i: '5', x: 0, y: 2, w: 2, h: 1 },
|
||||
{ i: '6', x: 2, y: 2, w: 1, h: 1 },
|
||||
{ i: '7', x: 3, y: 2, w: 1, h: 1 },
|
||||
],
|
||||
}
|
||||
return (
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={layouts}
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 0 }}
|
||||
cols={{ lg: 4, md: 3, sm: 2, xs: 1 }}
|
||||
rowHeight={300}
|
||||
>
|
||||
<div key="1">
|
||||
<FloatingElement>
|
||||
<TVChartContainer />
|
||||
</FloatingElement>
|
||||
</div>
|
||||
<div key="2">
|
||||
<FloatingElement>
|
||||
<Orderbook />
|
||||
</FloatingElement>
|
||||
</div>
|
||||
<div key="3">
|
||||
<TradeForm />
|
||||
</div>
|
||||
<div key="4">4</div>
|
||||
<div key="5">5</div>
|
||||
<div key="6">6</div>
|
||||
<div key="7">7</div>
|
||||
</ResponsiveGridLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default TradePageGrid
|
|
@ -6,7 +6,7 @@ import {
|
|||
ResolutionString,
|
||||
} from '../charting_library' // Make sure to follow step 1 of the README
|
||||
// import { useMarket } from '../../utils/markets';
|
||||
import { BONFIDA_DATA_FEED } from '../../utils/bonfidaConnector'
|
||||
import { CHART_DATA_FEED } from '../../utils/chartDataConnector'
|
||||
|
||||
// This is a basic example of how to create a TV widget
|
||||
// You can add more feature such as storing charts in localStorage
|
||||
|
@ -36,7 +36,7 @@ const TVChartContainer = () => {
|
|||
interval: '60' as ResolutionString,
|
||||
theme: 'Dark',
|
||||
containerId: 'tv_chart_container',
|
||||
datafeedUrl: BONFIDA_DATA_FEED,
|
||||
datafeedUrl: CHART_DATA_FEED,
|
||||
libraryPath: '/charting_library/',
|
||||
fullscreen: false,
|
||||
autosize: true,
|
||||
|
|
|
@ -1,33 +1,27 @@
|
|||
import { useEffect, useMemo } from 'react'
|
||||
import { Account, Connection } from '@solana/web3.js'
|
||||
import { IDS } from '@blockworks-foundation/mango-client'
|
||||
import { EndpointInfo } from '../@types/types'
|
||||
import useStore from './useStore'
|
||||
|
||||
export const ENDPOINTS: EndpointInfo[] = [
|
||||
{
|
||||
name: 'mainnet-beta',
|
||||
endpoint: 'https://solana-api.projectserum.com',
|
||||
custom: false,
|
||||
},
|
||||
{
|
||||
name: 'devnet',
|
||||
endpoint: 'https://devnet.solana.com',
|
||||
custom: false,
|
||||
},
|
||||
]
|
||||
|
||||
const cluster = 'mainnet-beta'
|
||||
import useSolanaStore from '../stores/useSolanaStore'
|
||||
|
||||
const useConnection = () => {
|
||||
const setConnection = useStore((state) => state.connection.setConnection)
|
||||
const { endpoint } = ENDPOINTS.find((e) => e.name === cluster)
|
||||
const connection = useMemo(() => new Connection(endpoint, 'recent'), [
|
||||
const { cluster, current: connection, endpoint } = useSolanaStore(
|
||||
(s) => s.connection
|
||||
)
|
||||
const setSolanaStore = useSolanaStore((s) => s.set)
|
||||
|
||||
const sendConnection = useMemo(() => new Connection(endpoint, 'recent'), [
|
||||
endpoint,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
setConnection(connection)
|
||||
// @ts-ignore
|
||||
if (connection && endpoint === connection._rpcEndpoint) return
|
||||
console.log('setting new connection')
|
||||
|
||||
const newConnection = new Connection(endpoint, 'recent')
|
||||
setSolanaStore((state) => {
|
||||
state.connection.current = newConnection
|
||||
})
|
||||
}, [endpoint])
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -47,7 +41,7 @@ const useConnection = () => {
|
|||
const programId = IDS[cluster].mango_program_id
|
||||
const dexProgramId = IDS[cluster]?.dex_program_id
|
||||
|
||||
return { connection, dexProgramId, cluster, programId }
|
||||
return { connection, dexProgramId, cluster, programId, sendConnection }
|
||||
}
|
||||
|
||||
export default useConnection
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function useIpAddress() {
|
||||
const [ipAllowed, setIpAllowed] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const checkIpLocation = async () => {
|
||||
const response = await fetch(`https://www.cloudflare.com/cdn-cgi/trace`)
|
||||
const parsedResponse = await response.text()
|
||||
const ipLocation = parsedResponse.match(/loc=(.+)/)
|
||||
const ipCountryCode = ipLocation ? ipLocation[1] : ''
|
||||
|
||||
if (ipCountryCode) {
|
||||
setIpAllowed(ipCountryCode !== 'US')
|
||||
}
|
||||
}
|
||||
|
||||
checkIpLocation()
|
||||
}, [])
|
||||
|
||||
return { ipAllowed }
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { IDS } from '@blockworks-foundation/mango-client'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import useConnection from './useConnection'
|
||||
import useInterval from './useInterval'
|
||||
import useSolanaStore from '../stores/useSolanaStore'
|
||||
|
||||
const useMarginAccount = () => {
|
||||
const mangoClient = useMangoStore((s) => s.mangoClient)
|
||||
const tradeForm = useMangoStore((s) => s.tradeForm)
|
||||
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup)
|
||||
const selectedMarginAccount = useMangoStore((s) => s.selectedMarginAccount)
|
||||
const mangoGroup = useMangoStore((s) => s.mangoGroup)
|
||||
const setMangoStore = useMangoStore((s) => s.set)
|
||||
|
||||
const { current: wallet } = useSolanaStore((s) => s.wallet)
|
||||
|
||||
const { cluster, connection } = useConnection()
|
||||
const clusterIds = useMemo(() => IDS[cluster], [cluster])
|
||||
const mangoGroupIds = useMemo(
|
||||
() => clusterIds.mango_groups[selectedMangoGroup],
|
||||
[clusterIds, selectedMangoGroup]
|
||||
)
|
||||
|
||||
const fetchMangoGroup = useCallback(() => {
|
||||
if (!mangoClient) return
|
||||
|
||||
const mangoGroupPk = new PublicKey(mangoGroupIds.mango_group_pk)
|
||||
const srmVaultPk = new PublicKey(mangoGroupIds.srm_vault_pk)
|
||||
|
||||
mangoClient
|
||||
.getMangoGroup(connection, mangoGroupPk, srmVaultPk)
|
||||
.then((mangoGroup) => {
|
||||
// Set the mango group
|
||||
setMangoStore((state) => {
|
||||
state.mangoGroup = mangoGroup
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Could not get mango group: ', err)
|
||||
})
|
||||
}, [connection, mangoClient, mangoGroupIds, setMangoStore])
|
||||
|
||||
const fetchMarginAccounts = useCallback(() => {
|
||||
if (!mangoClient || !wallet || !mangoGroup) return
|
||||
|
||||
mangoClient
|
||||
.getMarginAccountsForOwner(
|
||||
connection,
|
||||
new PublicKey(clusterIds.mango_program_id),
|
||||
mangoGroup,
|
||||
wallet
|
||||
)
|
||||
.then((marginAccounts) => {
|
||||
if (marginAccounts.length > 0) {
|
||||
setMangoStore((state) => {
|
||||
state.marginAcccounts = marginAccounts
|
||||
state.selectedMarginAccount = marginAccounts[0]
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Could not get margin accounts for user in effect ', err)
|
||||
})
|
||||
}, [mangoClient, connection, mangoGroup, wallet])
|
||||
|
||||
// useEffect(() => {
|
||||
// fetchMangoGroup()
|
||||
// }, [fetchMangoGroup])
|
||||
|
||||
// useInterval(() => {
|
||||
// fetchMarginAccounts()
|
||||
// fetchMangoGroup()
|
||||
// }, 20000)
|
||||
|
||||
return {
|
||||
mangoClient,
|
||||
setMangoStore,
|
||||
tradeForm,
|
||||
mangoGroup,
|
||||
marginAccount: selectedMarginAccount,
|
||||
}
|
||||
}
|
||||
|
||||
export default useMarginAccount
|
|
@ -0,0 +1,78 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import useInterval from './useInterval'
|
||||
import useSerumStore from '../stores/useSerumStore'
|
||||
import useOrderbook from './useOrderbook'
|
||||
import useConnection from './useConnection'
|
||||
|
||||
// const _VERY_SLOW_REFRESH_INTERVAL = 5000 * 1000;
|
||||
|
||||
// For things that don't really change
|
||||
const _SLOW_REFRESH_INTERVAL = 5 * 1000
|
||||
|
||||
// For things that change frequently
|
||||
// const _FAST_REFRESH_INTERVAL = 1000;
|
||||
|
||||
export function _useUnfilteredTrades(limit = 10000) {
|
||||
console.log('fetching unfiltered trades')
|
||||
|
||||
const market = useMangoStore((state) => state.market.current)
|
||||
const { connection } = useConnection()
|
||||
const setSerumStore = useSerumStore((state) => state.set)
|
||||
|
||||
useInterval(() => {
|
||||
async function fetchFills() {
|
||||
if (!market || !connection) {
|
||||
return null
|
||||
}
|
||||
const loadedFills = await market.loadFills(connection, limit)
|
||||
|
||||
setSerumStore((state) => {
|
||||
state.fills = loadedFills
|
||||
})
|
||||
}
|
||||
|
||||
fetchFills()
|
||||
}, _SLOW_REFRESH_INTERVAL)
|
||||
}
|
||||
|
||||
export function useTrades() {
|
||||
const trades = useSerumStore((state) => state.fills)
|
||||
if (!trades) {
|
||||
return null
|
||||
}
|
||||
// Until partial fills are each given their own fill, use maker fills
|
||||
return trades
|
||||
.filter(({ eventFlags }) => eventFlags.maker)
|
||||
.map((trade) => ({
|
||||
...trade,
|
||||
side: trade.side === 'buy' ? 'sell' : 'buy',
|
||||
}))
|
||||
}
|
||||
|
||||
export default function useMarkPrice() {
|
||||
const setMangoStore = useMangoStore((s) => s.set)
|
||||
const markPrice = useMangoStore((s) => s.market.markPrice)
|
||||
|
||||
const [orderbook] = useOrderbook()
|
||||
const trades = useTrades()
|
||||
|
||||
useEffect(() => {
|
||||
const bb = orderbook?.bids?.length > 0 && Number(orderbook.bids[0][0])
|
||||
const ba = orderbook?.asks?.length > 0 && Number(orderbook.asks[0][0])
|
||||
const last = trades && trades.length > 0 && trades[0].price
|
||||
|
||||
const newMarkPrice =
|
||||
bb && ba
|
||||
? last
|
||||
? [bb, ba, last].sort((a, b) => a - b)[1]
|
||||
: (bb + ba) / 2
|
||||
: null
|
||||
|
||||
setMangoStore((state) => {
|
||||
state.market.markPrice = newMarkPrice
|
||||
})
|
||||
}, [orderbook, trades])
|
||||
|
||||
return markPrice
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
import { useEffect } from 'react'
|
||||
import useConnection from './useConnection'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { Market } from '@project-serum/serum'
|
||||
import useStore from './useStore'
|
||||
import { IDS } from '@blockworks-foundation/mango-client'
|
||||
|
||||
const useMarkets = () => {
|
||||
const defaultMangoGroup = useStore((state) => state.defaultMangoGroup)
|
||||
const marketStore = useStore((state) => state.market)
|
||||
const { connection, cluster, programId, dexProgramId } = useConnection()
|
||||
|
||||
const spotMarkets =
|
||||
IDS[cluster]?.mango_groups[defaultMangoGroup]?.spot_market_symbols || {}
|
||||
|
||||
const marketList = Object.entries(spotMarkets).map(([name, address]) => {
|
||||
return {
|
||||
address: new PublicKey(address as string),
|
||||
programId: new PublicKey(dexProgramId as string),
|
||||
deprecated: false,
|
||||
name,
|
||||
}
|
||||
})
|
||||
|
||||
// TODO
|
||||
const defaultMarket = marketList[0]
|
||||
|
||||
useEffect(() => {
|
||||
Market.load(connection, defaultMarket.address, {}, defaultMarket.programId)
|
||||
.then((market) => {
|
||||
console.log('market loaded', market)
|
||||
marketStore.setMarket(market)
|
||||
})
|
||||
.catch(
|
||||
(e) => {
|
||||
console.log('failed to load market', e)
|
||||
}
|
||||
// TODO
|
||||
// notify({
|
||||
// message: 'Error loading market',
|
||||
// description: e.message,
|
||||
// type: 'error',
|
||||
// }),
|
||||
)
|
||||
}, [connection])
|
||||
|
||||
console.log('rerendering useMarkets hook', marketStore.market)
|
||||
|
||||
return { market: marketStore.market, programId, marketList }
|
||||
}
|
||||
|
||||
export default useMarkets
|
|
@ -0,0 +1,88 @@
|
|||
import { useEffect, useMemo } from 'react'
|
||||
import useConnection from './useConnection'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { Market } from '@project-serum/serum'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { IDS } from '@blockworks-foundation/mango-client'
|
||||
|
||||
const formatTokenMints = (symbols: { [name: string]: string }) => {
|
||||
return Object.entries(symbols).map(([name, address]) => {
|
||||
return {
|
||||
address: new PublicKey(address),
|
||||
name: name,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const useMarkets = () => {
|
||||
const setMangoStore = useMangoStore((state) => state.set)
|
||||
const selectedMangoGroup = useMangoStore((state) => state.selectedMangoGroup)
|
||||
const market = useMangoStore((state) => state.market.current)
|
||||
const { connection, cluster, programId, dexProgramId } = useConnection()
|
||||
|
||||
const spotMarkets =
|
||||
IDS[cluster]?.mango_groups[selectedMangoGroup]?.spot_market_symbols || {}
|
||||
|
||||
const marketList = useMemo(
|
||||
() =>
|
||||
Object.entries(spotMarkets).map(([name, address]) => {
|
||||
return {
|
||||
address: new PublicKey(address as string),
|
||||
programId: new PublicKey(dexProgramId as string),
|
||||
deprecated: false,
|
||||
name,
|
||||
}
|
||||
}),
|
||||
[spotMarkets]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (market) return
|
||||
console.log('loading market', connection)
|
||||
Market.load(connection, marketList[0].address, {}, marketList[0].programId)
|
||||
.then((market) => {
|
||||
setMangoStore((state) => {
|
||||
state.market.current = market
|
||||
})
|
||||
})
|
||||
.catch(
|
||||
(e) => {
|
||||
console.error('failed to load market', e)
|
||||
}
|
||||
// TODO
|
||||
// notify({
|
||||
// message: 'Error loading market',
|
||||
// description: e.message,
|
||||
// type: 'error',
|
||||
// }),
|
||||
)
|
||||
}, [connection])
|
||||
|
||||
const TOKEN_MINTS = useMemo(() => formatTokenMints(IDS[cluster].symbols), [
|
||||
cluster,
|
||||
])
|
||||
|
||||
const baseCurrency = useMemo(
|
||||
() =>
|
||||
(market?.baseMintAddress &&
|
||||
TOKEN_MINTS.find((token) =>
|
||||
token.address.equals(market.baseMintAddress)
|
||||
)?.name) ||
|
||||
'UNKNOWN',
|
||||
[market]
|
||||
)
|
||||
|
||||
const quoteCurrency = useMemo(
|
||||
() =>
|
||||
(market?.quoteMintAddress &&
|
||||
TOKEN_MINTS.find((token) =>
|
||||
token.address.equals(market.quoteMintAddress)
|
||||
)?.name) ||
|
||||
'UNKNOWN',
|
||||
[market]
|
||||
)
|
||||
|
||||
return { market, programId, marketList, baseCurrency, quoteCurrency }
|
||||
}
|
||||
|
||||
export default useMarkets
|
|
@ -1,75 +1,61 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import tuple from 'immutable-tuple'
|
||||
import { useEffect } from 'react'
|
||||
import { PublicKey, AccountInfo } from '@solana/web3.js'
|
||||
import { Orderbook } from '@project-serum/serum'
|
||||
import useMarkets from './useMarket'
|
||||
import useMarkets from './useMarkets'
|
||||
import useInterval from './useInterval'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import useSolanaStore from '../stores/useSolanaStore'
|
||||
import useConnection from './useConnection'
|
||||
import { setCache, useAsyncData } from '../utils/fetch-loop'
|
||||
|
||||
const accountListenerCount = new Map()
|
||||
|
||||
export function useAccountInfo(
|
||||
publicKey: PublicKey | undefined | null
|
||||
): [AccountInfo<Buffer> | null | undefined, boolean] {
|
||||
function useAccountInfo(account: PublicKey) {
|
||||
const setSolanaStore = useSolanaStore((s) => s.set)
|
||||
const { connection } = useConnection()
|
||||
const cacheKey = tuple(connection, publicKey?.toBase58())
|
||||
const [accountInfo, loaded] = useAsyncData<AccountInfo<Buffer> | null>(
|
||||
async () => (publicKey ? connection.getAccountInfo(publicKey) : null),
|
||||
cacheKey,
|
||||
{ refreshInterval: 60_000 }
|
||||
)
|
||||
const accountPkAsString = account ? account.toString() : null
|
||||
|
||||
useInterval(async () => {
|
||||
if (!account) return
|
||||
|
||||
const info = await connection.getAccountInfo(account)
|
||||
console.log('fetching account info on interval', accountPkAsString)
|
||||
|
||||
setSolanaStore((state) => {
|
||||
state.accountInfos[accountPkAsString] = info
|
||||
})
|
||||
}, 60000)
|
||||
|
||||
useEffect(() => {
|
||||
if (!publicKey) {
|
||||
return
|
||||
}
|
||||
if (accountListenerCount.has(cacheKey)) {
|
||||
const currentItem = accountListenerCount.get(cacheKey)
|
||||
++currentItem.count
|
||||
} else {
|
||||
let previousInfo: AccountInfo<Buffer> | null = null
|
||||
const subscriptionId = connection.onAccountChange(publicKey, (info) => {
|
||||
if (
|
||||
!previousInfo ||
|
||||
!previousInfo.data.equals(info.data) ||
|
||||
previousInfo.lamports !== info.lamports
|
||||
) {
|
||||
previousInfo = info
|
||||
setCache(cacheKey, info)
|
||||
}
|
||||
})
|
||||
accountListenerCount.set(cacheKey, { count: 1, subscriptionId })
|
||||
}
|
||||
return () => {
|
||||
const currentItem = accountListenerCount.get(cacheKey)
|
||||
const nextCount = currentItem.count - 1
|
||||
if (nextCount <= 0) {
|
||||
connection.removeAccountChangeListener(currentItem.subscriptionId)
|
||||
accountListenerCount.delete(cacheKey)
|
||||
} else {
|
||||
--currentItem.count
|
||||
if (!account) return
|
||||
let previousInfo: AccountInfo<Buffer> | null = null
|
||||
|
||||
const subscriptionId = connection.onAccountChange(account, (info) => {
|
||||
if (
|
||||
!previousInfo ||
|
||||
!previousInfo.data.equals(info.data) ||
|
||||
previousInfo.lamports !== info.lamports
|
||||
) {
|
||||
previousInfo = info
|
||||
setSolanaStore((state) => {
|
||||
state.accountInfos[accountPkAsString] = previousInfo
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
connection.removeAccountChangeListener(subscriptionId)
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [cacheKey])
|
||||
const previousInfoRef = useRef<AccountInfo<Buffer> | null | undefined>(null)
|
||||
if (
|
||||
!accountInfo ||
|
||||
!previousInfoRef.current ||
|
||||
!previousInfoRef.current.data.equals(accountInfo.data) ||
|
||||
previousInfoRef.current.lamports !== accountInfo.lamports
|
||||
) {
|
||||
previousInfoRef.current = accountInfo
|
||||
}
|
||||
return [previousInfoRef.current, loaded]
|
||||
}, [account, connection])
|
||||
}
|
||||
|
||||
export function useAccountData(publicKey) {
|
||||
const [accountInfo] = useAccountInfo(publicKey)
|
||||
return accountInfo && accountInfo.data
|
||||
useAccountInfo(publicKey)
|
||||
|
||||
const account = publicKey ? publicKey.toString() : null
|
||||
const accountInfo = useSolanaStore((s) => s.accountInfos[account])
|
||||
return accountInfo && Buffer.from(accountInfo.data)
|
||||
}
|
||||
|
||||
export function useOrderbookAccounts() {
|
||||
const { market } = useMarkets()
|
||||
const market = useMangoStore((s) => s.market.current)
|
||||
// @ts-ignore
|
||||
const bidData = useAccountData(market && market._decoded.bids)
|
||||
// @ts-ignore
|
||||
|
@ -85,6 +71,9 @@ export default function useOrderbook(
|
|||
): [{ bids: number[][]; asks: number[][] }, boolean] {
|
||||
const { bidOrderbook, askOrderbook } = useOrderbookAccounts()
|
||||
const { market } = useMarkets()
|
||||
|
||||
const setMangoStore = useMangoStore((s) => s.set)
|
||||
|
||||
const bids =
|
||||
!bidOrderbook || !market
|
||||
? []
|
||||
|
@ -93,5 +82,15 @@ export default function useOrderbook(
|
|||
!askOrderbook || !market
|
||||
? []
|
||||
: askOrderbook.getL2(depth).map(([price, size]) => [price, size])
|
||||
return [{ bids, asks }, !!bids || !!asks]
|
||||
|
||||
const orderBook: [{ bids: number[][]; asks: number[][] }, boolean] = [
|
||||
{ bids, asks },
|
||||
!!bids || !!asks,
|
||||
]
|
||||
|
||||
setMangoStore((state) => {
|
||||
state.market.orderBook = orderBook
|
||||
})
|
||||
|
||||
return orderBook
|
||||
}
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import create, { State } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { Connection } from '@solana/web3.js'
|
||||
import { Market } from '@project-serum/serum'
|
||||
|
||||
interface MangoStore extends State {
|
||||
wallet: {
|
||||
connected: boolean
|
||||
setConnected: (x: boolean) => void
|
||||
}
|
||||
connection: {
|
||||
endpoint: string
|
||||
connection: Connection
|
||||
setConnection: (x: Connection) => void
|
||||
}
|
||||
defaultMangoGroup: string
|
||||
market: {
|
||||
market: Market | null
|
||||
setMarket: (x: Market) => void
|
||||
}
|
||||
}
|
||||
|
||||
const useStore = create<MangoStore>(
|
||||
devtools((set) => ({
|
||||
wallet: {
|
||||
connected: false,
|
||||
setConnected: (connected) => {
|
||||
set((state) => ({ ...state, wallet: { ...state.wallet, connected } }))
|
||||
},
|
||||
},
|
||||
connection: {
|
||||
endpoint: 'devnet',
|
||||
connection: null,
|
||||
setConnection: (connection) => {
|
||||
set((state) => ({
|
||||
...state,
|
||||
connection: { ...state.connection, connection },
|
||||
}))
|
||||
},
|
||||
},
|
||||
defaultMangoGroup: 'BTC_ETH_USDT',
|
||||
market: {
|
||||
market: null,
|
||||
setMarket: (market) => {
|
||||
set((state) => ({
|
||||
...state,
|
||||
market: { ...state.market, market },
|
||||
}))
|
||||
},
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
export default useStore
|
|
@ -1,8 +1,8 @@
|
|||
import { useEffect, useMemo } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import Wallet from '@project-serum/sol-wallet-adapter'
|
||||
// import { notify } from './notifications'
|
||||
import useLocalStorageState from './useLocalStorageState'
|
||||
import useStore from './useStore'
|
||||
import useSolanaStore from '../stores/useSolanaStore'
|
||||
|
||||
export const WALLET_PROVIDERS = [
|
||||
{ name: 'sollet.io', url: 'https://www.sollet.io' },
|
||||
|
@ -10,32 +10,34 @@ export const WALLET_PROVIDERS = [
|
|||
|
||||
const ENDPOINT = process.env.CLUSTER ? process.env.CLUSTER : 'mainnet-beta'
|
||||
|
||||
export function useWallet() {
|
||||
const walletStore = useStore((state) => state.wallet)
|
||||
export default function useWallet() {
|
||||
const setSolanaStore = useSolanaStore((state) => state.set)
|
||||
const { current: wallet, connected } = useSolanaStore((state) => state.wallet)
|
||||
const endpoint = useSolanaStore((state) => state.connection.endpoint)
|
||||
const [savedProviderUrl] = useLocalStorageState(
|
||||
'walletProvider',
|
||||
'https://www.sollet.io'
|
||||
)
|
||||
|
||||
let providerUrl
|
||||
if (!savedProviderUrl) {
|
||||
providerUrl = 'https://www.sollet.io'
|
||||
} else {
|
||||
providerUrl = savedProviderUrl
|
||||
}
|
||||
|
||||
const wallet =
|
||||
typeof window !== 'undefined'
|
||||
? useMemo(() => new Wallet(providerUrl, ENDPOINT), [
|
||||
providerUrl,
|
||||
ENDPOINT,
|
||||
])
|
||||
: {}
|
||||
const providerUrl = savedProviderUrl
|
||||
? savedProviderUrl
|
||||
: 'https://www.sollet.io'
|
||||
|
||||
useEffect(() => {
|
||||
console.log('creating wallet', endpoint)
|
||||
|
||||
const newWallet = new Wallet(providerUrl, ENDPOINT)
|
||||
setSolanaStore((state) => {
|
||||
state.wallet.current = newWallet
|
||||
})
|
||||
}, [endpoint])
|
||||
|
||||
useEffect(() => {
|
||||
if (!wallet) return
|
||||
wallet.on('connect', () => {
|
||||
walletStore.setConnected(true)
|
||||
console.log('connected!!!!')
|
||||
setSolanaStore((state) => {
|
||||
state.wallet.connected = true
|
||||
})
|
||||
console.log('connected!')
|
||||
|
||||
// const walletPublicKey = wallet.publicKey.toBase58()
|
||||
// const keyToDisplay =
|
||||
|
@ -51,7 +53,9 @@ export function useWallet() {
|
|||
// })
|
||||
})
|
||||
wallet.on('disconnect', () => {
|
||||
walletStore.setConnected(false)
|
||||
setSolanaStore((state) => {
|
||||
state.wallet.connected = false
|
||||
})
|
||||
// notify({
|
||||
// message: 'Wallet update',
|
||||
// description: 'Disconnected from wallet',
|
||||
|
@ -60,9 +64,11 @@ export function useWallet() {
|
|||
})
|
||||
return () => {
|
||||
wallet.disconnect()
|
||||
walletStore.setConnected(false)
|
||||
setSolanaStore((state) => {
|
||||
state.wallet.connected = false
|
||||
})
|
||||
}
|
||||
}, [wallet])
|
||||
|
||||
return wallet
|
||||
return { wallet, connected }
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
module.exports = {
|
||||
const withAntdLess = require('next-plugin-antd-less')
|
||||
|
||||
module.exports = withAntdLess({
|
||||
lessVarsFilePath: './styles/theme.less',
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
|
@ -7,4 +10,4 @@ module.exports = {
|
|||
|
||||
return config
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -29,11 +29,17 @@
|
|||
"@blockworks-foundation/mango-client": "^0.1.10",
|
||||
"@emotion/react": "^11.1.5",
|
||||
"@emotion/styled": "^11.1.5",
|
||||
"@heroicons/react": "^1.0.0",
|
||||
"@project-serum/serum": "^0.13.31",
|
||||
"@project-serum/sol-wallet-adapter": "^0.1.8",
|
||||
"@solana/web3.js": "^1.2.3",
|
||||
"antd": "^4.15.0",
|
||||
"babel-plugin-import": "^1.13.3",
|
||||
"bn.js": "^5.2.0",
|
||||
"immer": "^9.0.1",
|
||||
"immutable-tuple": "^0.4.10",
|
||||
"next": "latest",
|
||||
"next-plugin-antd-less": "^0.3.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-grid-layout": "^1.2.4",
|
||||
|
@ -54,6 +60,7 @@
|
|||
"eslint": "^7.19.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"husky": "^4.2.3",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import Head from 'next/head'
|
||||
import { Global } from '@emotion/react'
|
||||
import xw from 'xwind'
|
||||
import './index.css'
|
||||
import '../node_modules/react-grid-layout/css/styles.css'
|
||||
import '../node_modules/react-resizable/css/styles.css'
|
||||
import '../styles/index.css'
|
||||
|
||||
function App({ Component, pageProps }) {
|
||||
return (
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
.TVChartContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tradingview-chart {
|
||||
display: contents;
|
||||
}
|
|
@ -1,67 +1,21 @@
|
|||
import xw from 'xwind'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Responsive, WidthProvider } from 'react-grid-layout'
|
||||
|
||||
const TVChartContainer = dynamic(
|
||||
() => import('../components/TradingView/index'),
|
||||
{ ssr: false }
|
||||
)
|
||||
import TopBar from '../components/TopBar'
|
||||
import Notifications from '../components/Notification'
|
||||
import FloatingElement from '../components/FloatingElement'
|
||||
// import Orderbook from '../components/Orderbook'
|
||||
import useConnection from '../hooks/useConnection'
|
||||
import useMarkets from '../hooks/useMarket'
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive)
|
||||
|
||||
const LAYOUTS = {
|
||||
lg: [
|
||||
{ i: '1', x: 0, y: 0, w: 2, h: 2 },
|
||||
{ i: '2', x: 2, y: 0, w: 1, h: 2 },
|
||||
{ i: '3', x: 3, y: 0, w: 1, h: 1 },
|
||||
{ i: '4', x: 3, y: 1, w: 1, h: 1 },
|
||||
{ i: '5', x: 0, y: 2, w: 2, h: 1 },
|
||||
{ i: '6', x: 2, y: 2, w: 1, h: 1 },
|
||||
{ i: '7', x: 3, y: 2, w: 1, h: 1 },
|
||||
],
|
||||
}
|
||||
import TradePageGrid from '../components/TradePageGrid'
|
||||
|
||||
const Index = () => {
|
||||
useConnection()
|
||||
useMarkets()
|
||||
|
||||
return (
|
||||
<div css={xw`bg-mango-dark text-white`}>
|
||||
<TopBar />
|
||||
<div css={xw`min-h-screen p-1 sm:p-2 md:p-6`}>
|
||||
<TradePageGrid />
|
||||
</div>
|
||||
<Notifications
|
||||
notifications={[
|
||||
{ title: 'test', message: 'ok' },
|
||||
{ title: 'test2', message: 'ok2' },
|
||||
]}
|
||||
/>
|
||||
<TopBar />
|
||||
<div css={xw`min-h-screen p-6`}>
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={LAYOUTS}
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 0 }}
|
||||
cols={{ lg: 4, md: 3, sm: 2, xs: 1 }}
|
||||
>
|
||||
<div key="1">
|
||||
<FloatingElement>
|
||||
<TVChartContainer />
|
||||
</FloatingElement>
|
||||
</div>
|
||||
<div key="2">
|
||||
<FloatingElement>{/* <Orderbook /> */}</FloatingElement>
|
||||
</div>
|
||||
<div key="3">3</div>
|
||||
<div key="4">4</div>
|
||||
<div key="5">5</div>
|
||||
<div key="6">6</div>
|
||||
<div key="7">7</div>
|
||||
</ResponsiveGridLayout>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import create, { State } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import produce from 'immer'
|
||||
import { Market } from '@project-serum/serum'
|
||||
import {
|
||||
MangoClient,
|
||||
MangoGroup,
|
||||
MarginAccount,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
|
||||
interface MangoStore extends State {
|
||||
selectedMangoGroup: string
|
||||
market: {
|
||||
current: Market | null
|
||||
markPrice: number
|
||||
orderBook: any[]
|
||||
}
|
||||
mangoClient: MangoClient
|
||||
mangoGroup: MangoGroup | null
|
||||
selectedMarginAcccount: MarginAccount | null
|
||||
tradeForm: {
|
||||
currency: string
|
||||
size: number
|
||||
}
|
||||
set: (x: any) => void
|
||||
}
|
||||
|
||||
const useMangoStore = create<MangoStore>(
|
||||
devtools((set) => ({
|
||||
selectedMangoGroup: 'BTC_ETH_USDT',
|
||||
market: {
|
||||
current: null,
|
||||
markPrice: 0,
|
||||
orderBook: [],
|
||||
},
|
||||
mangoClient: new MangoClient(),
|
||||
mangoGroup: null,
|
||||
marginAccounts: [],
|
||||
selectedMarginAcccount: null,
|
||||
tradeForm: {
|
||||
size: 0,
|
||||
currency: 'BTC',
|
||||
},
|
||||
set: (fn) => set(produce(fn)),
|
||||
}))
|
||||
)
|
||||
|
||||
export default useMangoStore
|
|
@ -0,0 +1,27 @@
|
|||
import create, { State } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import produce from 'immer'
|
||||
// import { Connection } from '@solana/web3.js'
|
||||
// import { Market } from '@project-serum/serum'
|
||||
|
||||
interface SerumStore extends State {
|
||||
orderbook: {
|
||||
bids: any[]
|
||||
asks: any[]
|
||||
}
|
||||
fills: any[]
|
||||
set: (x: any) => void
|
||||
}
|
||||
|
||||
const useSerumStore = create<SerumStore>(
|
||||
devtools((set) => ({
|
||||
orderbook: {
|
||||
bids: [],
|
||||
asks: [],
|
||||
},
|
||||
fills: [],
|
||||
set: (fn) => set(produce(fn)),
|
||||
}))
|
||||
)
|
||||
|
||||
export default useSerumStore
|
|
@ -0,0 +1,59 @@
|
|||
import create, { State } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import produce from 'immer'
|
||||
import { AccountInfo, Connection } from '@solana/web3.js'
|
||||
import { Wallet } from '@project-serum/sol-wallet-adapter'
|
||||
import { EndpointInfo } from '../@types/types'
|
||||
|
||||
export const ENDPOINTS: EndpointInfo[] = [
|
||||
{
|
||||
name: 'mainnet-beta',
|
||||
endpoint: 'https://solana-api.projectserum.com',
|
||||
custom: false,
|
||||
},
|
||||
{
|
||||
name: 'devnet',
|
||||
endpoint: 'https://devnet.solana.com',
|
||||
custom: false,
|
||||
},
|
||||
]
|
||||
|
||||
const CLUSTER = 'mainnet-beta'
|
||||
const ENDPOINT_URL = ENDPOINTS.find((e) => e.name === CLUSTER).endpoint
|
||||
const DEFAULT_CONNECTION = new Connection(ENDPOINT_URL, 'recent')
|
||||
|
||||
interface AccountInfoList {
|
||||
[key: string]: AccountInfo<Buffer>
|
||||
}
|
||||
|
||||
interface SolanaStore extends State {
|
||||
accountInfos: AccountInfoList
|
||||
connection: {
|
||||
cluster: string
|
||||
current: Connection
|
||||
endpoint: string
|
||||
}
|
||||
wallet: {
|
||||
connected: boolean
|
||||
current: Wallet
|
||||
}
|
||||
set: (x: any) => void
|
||||
}
|
||||
|
||||
const useSolanaStore = create<SolanaStore>(
|
||||
devtools((set) => ({
|
||||
accountInfos: {},
|
||||
connection: {
|
||||
cluster: CLUSTER,
|
||||
current: DEFAULT_CONNECTION,
|
||||
endpoint: ENDPOINT_URL,
|
||||
},
|
||||
wallet: {
|
||||
connected: false,
|
||||
current: null,
|
||||
},
|
||||
set: (fn) => set(produce(fn)),
|
||||
}))
|
||||
)
|
||||
|
||||
export default useSolanaStore
|
|
@ -0,0 +1,19 @@
|
|||
.TVChartContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tradingview-chart {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.react-grid-item.react-grid-placeholder {
|
||||
background: #332f46;
|
||||
transition-duration: 100ms;
|
||||
z-index: 2;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// colors
|
||||
@primary-color: #f2c94c;
|
||||
@success-color: #afd803;
|
||||
@error-color: #e54033;
|
||||
@text-color: #eeeeee;
|
||||
@text-color-secondary: #9490a6;
|
||||
@border-color-base: #584f81;
|
||||
@divider-color: #584f81;
|
||||
@btn-primary-color: #141026;
|
||||
@body-background: #141026;
|
||||
@component-background: #141026;
|
||||
@layout-body-background: #141026;
|
||||
|
||||
// font
|
||||
@font-family: 'Lato', sans-serif;
|
||||
/*@font-size-base: 21px;*/
|
||||
|
||||
// layout
|
||||
@border-radius-base: 9px;
|
||||
@layout-header-height: 40px;
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
@ -23,6 +23,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"exclude": ["node_modules", ".next", "out"],
|
||||
"exclude": ["node_modules", ".next", "out", "public/datafeeds/udf/src"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"]
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { BonfidaTrade } from '../@types/types'
|
||||
import { ChartType } from '../@types/types'
|
||||
|
||||
const baseUrl = 'https://serum-history.herokuapp.com'
|
||||
//const baseUrl = "http://85.214.116.56:5000";
|
||||
//const baseUrl = "http://localhost:5000";
|
||||
|
||||
export default class BonfidaApi {
|
||||
export default class ChartApi {
|
||||
static URL = `${baseUrl}/`
|
||||
|
||||
static async get(path: string) {
|
||||
|
@ -15,16 +12,16 @@ export default class BonfidaApi {
|
|||
return responseJson.success ? responseJson.data : null
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error fetching from Bonfida API ${path}: ${err}`)
|
||||
console.log(`Error fetching from Chart API ${path}: ${err}`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static async getRecentTrades(
|
||||
marketAddress: string
|
||||
): Promise<BonfidaTrade[] | null> {
|
||||
return BonfidaApi.get(`trades/address/${marketAddress}`)
|
||||
): Promise<ChartType[] | null> {
|
||||
return ChartApi.get(`trades/address/${marketAddress}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const BONFIDA_DATA_FEED = `${baseUrl}/tv`
|
||||
export const CHART_DATA_FEED = `${baseUrl}/tv`
|
|
@ -203,7 +203,6 @@ export function useAsyncData<T = any>(
|
|||
|
||||
useEffect(() => {
|
||||
if (!cacheKey) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {}
|
||||
}
|
||||
const listener = new FetchLoopListener<T>(
|
||||
|
@ -216,7 +215,7 @@ export function useAsyncData<T = any>(
|
|||
)
|
||||
globalLoops.addListener(listener)
|
||||
return () => globalLoops.removeListener(listener)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line
|
||||
}, [cacheKey, refreshInterval])
|
||||
|
||||
if (!cacheKey) {
|
||||
|
|
|
@ -112,3 +112,25 @@ export function groupBy(list, keyGetter) {
|
|||
export function isDefined<T>(argument: T | undefined): argument is T {
|
||||
return argument !== undefined
|
||||
}
|
||||
|
||||
export const calculateMarketPrice = (
|
||||
orderBook: Array<any>,
|
||||
size: number,
|
||||
side: string
|
||||
) => {
|
||||
let acc = 0
|
||||
let selectedOrder
|
||||
for (const order of orderBook) {
|
||||
acc += order[1]
|
||||
if (acc >= size) {
|
||||
selectedOrder = order
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (side === 'buy') {
|
||||
return selectedOrder[0] * 1.05
|
||||
} else {
|
||||
return selectedOrder[0] * 0.95
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
|||
export function notify(args) {
|
||||
alert(`notify: ${args}`)
|
||||
}
|
|
@ -0,0 +1,378 @@
|
|||
import { notify } from './notifications'
|
||||
import { sleep } from './index'
|
||||
import {
|
||||
Account,
|
||||
AccountInfo,
|
||||
Commitment,
|
||||
Connection,
|
||||
PublicKey,
|
||||
RpcResponseAndContext,
|
||||
SimulatedTransactionResponse,
|
||||
Transaction,
|
||||
TransactionSignature,
|
||||
} from '@solana/web3.js'
|
||||
import Wallet from '@project-serum/sol-wallet-adapter'
|
||||
import { Buffer } from 'buffer'
|
||||
import assert from 'assert'
|
||||
import { struct } from 'superstruct'
|
||||
|
||||
export const getUnixTs = () => {
|
||||
return new Date().getTime() / 1000
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT = 30000
|
||||
|
||||
export async function sendTransaction({
|
||||
transaction,
|
||||
wallet,
|
||||
signers = [],
|
||||
connection,
|
||||
sendingMessage = 'Sending transaction...',
|
||||
sentMessage = 'Transaction sent',
|
||||
successMessage = 'Transaction confirmed',
|
||||
timeout = DEFAULT_TIMEOUT,
|
||||
}: {
|
||||
transaction: Transaction
|
||||
wallet: Wallet
|
||||
signers?: Array<Account>
|
||||
connection: Connection
|
||||
sendingMessage?: string
|
||||
sentMessage?: string
|
||||
successMessage?: string
|
||||
timeout?: number
|
||||
}) {
|
||||
const signedTransaction = await signTransaction({
|
||||
transaction,
|
||||
wallet,
|
||||
signers,
|
||||
connection,
|
||||
})
|
||||
return await sendSignedTransaction({
|
||||
signedTransaction,
|
||||
connection,
|
||||
sendingMessage,
|
||||
sentMessage,
|
||||
successMessage,
|
||||
timeout,
|
||||
})
|
||||
}
|
||||
|
||||
export async function signTransaction({
|
||||
transaction,
|
||||
wallet,
|
||||
signers = [],
|
||||
connection,
|
||||
}: {
|
||||
transaction: Transaction
|
||||
wallet: Wallet
|
||||
signers?: Array<Account>
|
||||
connection: Connection
|
||||
}) {
|
||||
transaction.recentBlockhash = (
|
||||
await connection.getRecentBlockhash('max')
|
||||
).blockhash
|
||||
transaction.setSigners(wallet.publicKey, ...signers.map((s) => s.publicKey))
|
||||
if (signers.length > 0) {
|
||||
transaction.partialSign(...signers)
|
||||
}
|
||||
return await wallet.signTransaction(transaction)
|
||||
}
|
||||
|
||||
export async function signTransactions({
|
||||
transactionsAndSigners,
|
||||
wallet,
|
||||
connection,
|
||||
}: {
|
||||
transactionsAndSigners: {
|
||||
transaction: Transaction
|
||||
signers?: Array<Account>
|
||||
}[]
|
||||
wallet: Wallet
|
||||
connection: Connection
|
||||
}) {
|
||||
const blockhash = (await connection.getRecentBlockhash('max')).blockhash
|
||||
transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
|
||||
transaction.recentBlockhash = blockhash
|
||||
transaction.setSigners(wallet.publicKey, ...signers.map((s) => s.publicKey))
|
||||
if (signers?.length > 0) {
|
||||
transaction.partialSign(...signers)
|
||||
}
|
||||
})
|
||||
return await wallet.signAllTransactions(
|
||||
transactionsAndSigners.map(({ transaction }) => transaction)
|
||||
)
|
||||
}
|
||||
|
||||
export async function sendSignedTransaction({
|
||||
signedTransaction,
|
||||
connection,
|
||||
sendingMessage = 'Sending transaction...',
|
||||
sentMessage = 'Transaction sent',
|
||||
successMessage = 'Transaction confirmed',
|
||||
timeout = DEFAULT_TIMEOUT,
|
||||
}: {
|
||||
signedTransaction: Transaction
|
||||
connection: Connection
|
||||
sendingMessage?: string
|
||||
sentMessage?: string
|
||||
successMessage?: string
|
||||
timeout?: number
|
||||
}): Promise<string> {
|
||||
const rawTransaction = signedTransaction.serialize()
|
||||
const startTime = getUnixTs()
|
||||
notify({ message: sendingMessage })
|
||||
const txid: TransactionSignature = await connection.sendRawTransaction(
|
||||
rawTransaction,
|
||||
{
|
||||
skipPreflight: true,
|
||||
}
|
||||
)
|
||||
notify({ message: sentMessage, type: 'success', txid })
|
||||
|
||||
console.log('Started awaiting confirmation for', txid)
|
||||
|
||||
let done = false
|
||||
;(async () => {
|
||||
while (!done && getUnixTs() - startTime < timeout) {
|
||||
connection.sendRawTransaction(rawTransaction, {
|
||||
skipPreflight: true,
|
||||
})
|
||||
await sleep(300)
|
||||
}
|
||||
})()
|
||||
try {
|
||||
await awaitTransactionSignatureConfirmation(txid, timeout, connection)
|
||||
} catch (err) {
|
||||
if (err.timeout) {
|
||||
throw new Error('Timed out awaiting confirmation on transaction')
|
||||
}
|
||||
let simulateResult: SimulatedTransactionResponse | null = null
|
||||
try {
|
||||
simulateResult = (
|
||||
await simulateTransaction(connection, signedTransaction, 'single')
|
||||
).value
|
||||
} catch (e) {
|
||||
console.log('Error: ', e)
|
||||
}
|
||||
if (simulateResult && simulateResult.err) {
|
||||
if (simulateResult.logs) {
|
||||
for (let i = simulateResult.logs.length - 1; i >= 0; --i) {
|
||||
const line = simulateResult.logs[i]
|
||||
if (line.startsWith('Program log: ')) {
|
||||
throw new Error(
|
||||
'Transaction failed: ' + line.slice('Program log: '.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(JSON.stringify(simulateResult.err))
|
||||
}
|
||||
throw new Error('Transaction failed')
|
||||
} finally {
|
||||
done = true
|
||||
}
|
||||
notify({ message: successMessage, type: 'success', txid })
|
||||
|
||||
console.log('Latency', txid, getUnixTs() - startTime)
|
||||
return txid
|
||||
}
|
||||
|
||||
async function awaitTransactionSignatureConfirmation(
|
||||
txid: TransactionSignature,
|
||||
timeout: number,
|
||||
connection: Connection
|
||||
) {
|
||||
let done = false
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line
|
||||
;(async () => {
|
||||
setTimeout(() => {
|
||||
if (done) {
|
||||
return
|
||||
}
|
||||
done = true
|
||||
console.log('Timed out for txid', txid)
|
||||
reject({ timeout: true })
|
||||
}, timeout)
|
||||
try {
|
||||
connection.onSignature(
|
||||
txid,
|
||||
(result) => {
|
||||
console.log('WS confirmed', txid, result)
|
||||
done = true
|
||||
if (result.err) {
|
||||
reject(result.err)
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
},
|
||||
connection.commitment
|
||||
)
|
||||
console.log('Set up WS connection', txid)
|
||||
} catch (e) {
|
||||
done = true
|
||||
console.log('WS error in setup', txid, e)
|
||||
}
|
||||
while (!done) {
|
||||
// eslint-disable-next-line
|
||||
;(async () => {
|
||||
try {
|
||||
const signatureStatuses = await connection.getSignatureStatuses([
|
||||
txid,
|
||||
])
|
||||
const result = signatureStatuses && signatureStatuses.value[0]
|
||||
if (!done) {
|
||||
if (!result) {
|
||||
// console.log('REST null result for', txid, result);
|
||||
} else if (result.err) {
|
||||
console.log('REST error for', txid, result)
|
||||
done = true
|
||||
reject(result.err)
|
||||
}
|
||||
// @ts-ignore
|
||||
else if (
|
||||
!(
|
||||
result.confirmations ||
|
||||
result.confirmationStatus === 'confirmed' ||
|
||||
result.confirmationStatus === 'finalized'
|
||||
)
|
||||
) {
|
||||
console.log('REST not confirmed', txid, result)
|
||||
} else {
|
||||
console.log('REST confirmed', txid, result)
|
||||
done = true
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!done) {
|
||||
console.log('REST connection error: txid', txid, e)
|
||||
}
|
||||
}
|
||||
})()
|
||||
await sleep(300)
|
||||
}
|
||||
})()
|
||||
})
|
||||
done = true
|
||||
return result
|
||||
}
|
||||
|
||||
function jsonRpcResult(resultDescription: any) {
|
||||
const jsonRpcVersion = struct.literal('2.0')
|
||||
return struct.union([
|
||||
struct({
|
||||
jsonrpc: jsonRpcVersion,
|
||||
id: 'string',
|
||||
error: 'any',
|
||||
}),
|
||||
struct({
|
||||
jsonrpc: jsonRpcVersion,
|
||||
id: 'string',
|
||||
error: 'null?',
|
||||
result: resultDescription,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
function jsonRpcResultAndContext(resultDescription: any) {
|
||||
return jsonRpcResult({
|
||||
context: struct({
|
||||
slot: 'number',
|
||||
}),
|
||||
value: resultDescription,
|
||||
})
|
||||
}
|
||||
|
||||
const AccountInfoResult = struct({
|
||||
executable: 'boolean',
|
||||
owner: 'string',
|
||||
lamports: 'number',
|
||||
data: 'any',
|
||||
rentEpoch: 'number?',
|
||||
})
|
||||
|
||||
export const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext(
|
||||
struct.array([struct.union(['null', AccountInfoResult])])
|
||||
)
|
||||
|
||||
export async function getMultipleSolanaAccounts(
|
||||
connection: Connection,
|
||||
publicKeys: PublicKey[]
|
||||
): Promise<
|
||||
RpcResponseAndContext<{ [key: string]: AccountInfo<Buffer> | null }>
|
||||
> {
|
||||
const args = [publicKeys.map((k) => k.toBase58()), { commitment: 'recent' }]
|
||||
// @ts-ignore
|
||||
const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args)
|
||||
const res = GetMultipleAccountsAndContextRpcResult(unsafeRes)
|
||||
if (res.error) {
|
||||
throw new Error(
|
||||
'failed to get info about accounts ' +
|
||||
publicKeys.map((k) => k.toBase58()).join(', ') +
|
||||
': ' +
|
||||
res.error.message
|
||||
)
|
||||
}
|
||||
assert(typeof res.result !== 'undefined')
|
||||
const accounts: Array<{
|
||||
executable: any
|
||||
owner: PublicKey
|
||||
lamports: any
|
||||
data: Buffer
|
||||
} | null> = []
|
||||
for (const account of res.result.value) {
|
||||
let value: {
|
||||
executable: any
|
||||
owner: PublicKey
|
||||
lamports: any
|
||||
data: Buffer
|
||||
} | null = null
|
||||
if (res.result.value) {
|
||||
const { executable, owner, lamports, data } = account
|
||||
assert(data[1] === 'base64')
|
||||
value = {
|
||||
executable,
|
||||
owner: new PublicKey(owner),
|
||||
lamports,
|
||||
data: Buffer.from(data[0], 'base64'),
|
||||
}
|
||||
}
|
||||
accounts.push(value)
|
||||
}
|
||||
return {
|
||||
context: {
|
||||
slot: res.result.context.slot,
|
||||
},
|
||||
value: Object.fromEntries(
|
||||
accounts.map((account, i) => [publicKeys[i].toBase58(), account])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy of Connection.simulateTransaction that takes a commitment parameter. */
|
||||
async function simulateTransaction(
|
||||
connection: Connection,
|
||||
transaction: Transaction,
|
||||
commitment: Commitment
|
||||
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
|
||||
// @ts-ignore
|
||||
transaction.recentBlockhash = await connection._recentBlockhash(
|
||||
// @ts-ignore
|
||||
connection._disableBlockhashCaching
|
||||
)
|
||||
|
||||
const signData = transaction.serializeMessage()
|
||||
// @ts-ignore
|
||||
const wireTransaction = transaction._serialize(signData)
|
||||
const encodedTransaction = wireTransaction.toString('base64')
|
||||
const config: any = { encoding: 'base64', commitment }
|
||||
const args = [encodedTransaction, config]
|
||||
|
||||
// @ts-ignore
|
||||
const res = await connection._rpcRequest('simulateTransaction', args)
|
||||
if (res.error) {
|
||||
throw new Error('failed to simulate transaction: ' + res.error.message)
|
||||
}
|
||||
return res.result
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { PublicKey } from '@solana/web3.js'
|
||||
|
||||
export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
||||
)
|
Loading…
Reference in New Issue