Reorganize context providers

This commit is contained in:
armaniferrante 2021-05-13 21:39:07 -07:00
parent 265ee7f720
commit 94ded3ab69
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
9 changed files with 403 additions and 356 deletions

View File

@ -1,322 +0,0 @@
import React, { useContext, useState, useEffect } from "react";
import { useAsync } from "react-async-hook";
import * as anchor from "@project-serum/anchor";
import { Provider } from "@project-serum/anchor";
import { Swap as SwapClient } from "@project-serum/swap";
import { Market, OpenOrders } from "@project-serum/serum";
import { PublicKey, Account } from "@solana/web3.js";
import {
AccountInfo as TokenAccount,
MintInfo,
Token,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { TokenListContainer, TokenInfo } from "@solana/spl-token-registry";
import { getOwnedTokenAccounts } from "../utils/tokens";
const SRM_MINT = new PublicKey("SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt");
export const USDC_MINT = new PublicKey(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
);
export const USDT_MINT = new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
);
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
const SwapContext = React.createContext<null | SwapContext>(null);
export function SwapContextProvider(props: any) {
const swapClient = props.swapClient;
const [fromMint, setFromMint] = useState(SRM_MINT);
const [toMint, setToMint] = useState(USDC_MINT);
const [fromAmount, setFromAmount] = useState(0);
const [toAmount, setToAmount] = useState(0);
const [fromBalance, setFromBalance] = useState(undefined);
const [toBalance, setToBalance] = useState(undefined);
const [minExpectedAmount, setMinExpectedAmount] = useState(0);
const [ownedTokenAccounts, setOwnedTokenAccounts] = useState(undefined);
const [slippage, setSlippage] = useState(0.5);
// Fetch all the owned token accounts for the wallet.
useEffect(() => {
getOwnedTokenAccounts(
swapClient.program.provider.connection,
swapClient.program.provider.wallet.publicKey
).then(setOwnedTokenAccounts);
}, [
swapClient.program.provider.wallet.publicKey,
swapClient.program.provider.connection,
]);
const swapToFromMints = () => {
const oldFrom = fromMint;
const oldFromAmount = fromAmount;
const oldTo = toMint;
const oldToAmount = toAmount;
setFromMint(oldTo);
setToMint(oldFrom);
setFromAmount(oldToAmount);
setToAmount(oldFromAmount);
};
return (
<SwapContext.Provider
value={{
swapClient,
fromMint,
setFromMint,
toMint,
setToMint,
fromAmount,
setFromAmount,
toAmount,
setToAmount,
minExpectedAmount,
swapToFromMints,
fromBalance,
toBalance,
ownedTokenAccounts,
slippage,
setSlippage,
}}
>
{props.children}
</SwapContext.Provider>
);
}
export function useSwapContext(): SwapContext {
const ctx = useContext(SwapContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx;
}
export type SwapContext = {
swapClient: SwapClient;
fromMint: PublicKey;
setFromMint: (m: PublicKey) => void;
toMint: PublicKey;
setToMint: (m: PublicKey) => void;
fromAmount: number;
setFromAmount: (a: number) => void;
toAmount: number;
setToAmount: (a: number) => void;
minExpectedAmount: number;
swapToFromMints: () => void;
fromBalance?: number;
toBalance?: number;
fromMintAccount?: MintInfo;
toMintAccount?: MintInfo;
ownedTokenAccounts:
| { publicKey: PublicKey; account: TokenAccount }[]
| undefined;
slippage: number;
setSlippage: (n: number) => void;
};
const TokenListContext = React.createContext<null | TokenListContext>(null);
export function TokenListContextProvider(props: any) {
return (
<TokenListContext.Provider value={{ tokenList: props.tokenList }}>
{props.children}
</TokenListContext.Provider>
);
}
type TokenListContext = {
tokenList: TokenListContainer;
};
export function useTokenList(): TokenInfo[] {
const ctx = useContext(TokenListContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx.tokenList.getList();
}
// Null => none exists.
// Undefined => loading.
export function useOwnedTokenAccount(
mint: PublicKey
): { publicKey: PublicKey; account: TokenAccount } | null | undefined {
const ctx = useContext(SwapContext);
if (ctx === null) {
throw new Error("Context not available");
}
if (ctx.ownedTokenAccounts === undefined) {
return undefined;
}
const tokenAccounts = ctx.ownedTokenAccounts.filter((account) =>
account.account.mint.equals(mint)
);
if (tokenAccounts.length === 0) {
return null;
}
// Take the account with the most tokens in it.
tokenAccounts.sort((a, b) =>
a.account.amount < a.account.amount
? -1
: a.account.amount > b.account.amount
? 1
: 0
);
return tokenAccounts[0];
}
const MintContext = React.createContext<null | MintContext>(null);
type MintContext = {
mintCache: Map<string, MintInfo>;
setMintCache: (m: Map<string, MintInfo>) => void;
provider: Provider;
};
export function MintContextProvider(props: any) {
const provider = props.provider;
const [mintCache, setMintCache] = useState(new Map<string, MintInfo>());
return (
<MintContext.Provider
value={{
mintCache,
setMintCache,
provider,
}}
>
{props.children}
</MintContext.Provider>
);
}
export function useMint(mint: PublicKey): MintInfo | undefined | null {
const ctx = useContext(MintContext);
if (ctx === null) {
throw new Error("Mint context not found");
}
// Lazy load the mint account if needeed.
const asyncMintInfo = useAsync(async () => {
if (ctx.mintCache.get(mint.toString())) {
return ctx.mintCache.get(mint.toString());
}
const mintClient = new Token(
ctx.provider.connection,
mint,
TOKEN_PROGRAM_ID,
new Account()
);
const mintInfo = await mintClient.getMintInfo();
let cache = new Map(ctx.mintCache);
cache.set(mint.toString(), mintInfo);
ctx.setMintCache(cache);
return mintInfo;
}, [ctx.provider.connection, mint]);
if (asyncMintInfo.result) {
return asyncMintInfo.result;
}
return undefined;
}
const SerumDexContext = React.createContext<SerumDexContext | null>(null);
type SerumDexContext = {
// Maps market address to open orders accounts.
openOrders: Map<string, Array<OpenOrders>>;
marketCache: Map<string, Market>;
};
export function useOpenOrders(): Map<string, Array<OpenOrders>> {
const ctx = useContext(SerumDexContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx.openOrders;
}
export function useMarket(market: PublicKey): Market | undefined {
const ctx = useContext(SerumDexContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx.marketCache.get(market.toString());
}
export function SerumDexContextProvider(props: any) {
const [ooAccounts, setOoAccounts] = useState<Map<string, Array<OpenOrders>>>(
new Map()
);
const [marketCache, setMarketCache] = useState<Map<string, Market>>(
new Map()
);
const provider = props.provider;
// Two operations:
// 1. Fetch all open orders accounts for the connected wallet.
// 2. Batch fetch all market accounts.
useEffect(() => {
OpenOrders.findForOwner(
provider.connection,
provider.wallet.publicKey,
DEX_PID
).then(async (openOrders) => {
const newOoAccounts = new Map();
let markets = new Set<string>();
openOrders.forEach((oo) => {
markets.add(oo.market.toString());
if (newOoAccounts.get(oo.market.toString())) {
newOoAccounts.get(oo.market.toString()).push(oo);
} else {
newOoAccounts.set(oo.market.toString(), [oo]);
}
});
if (markets.size > 100) {
// Punt request chunking until there's user demand.
throw new Error(
"Too many markets. Please file an issue to update this"
);
}
const marketAccounts = (
await anchor.utils.getMultipleAccounts(
provider.connection,
// @ts-ignore
[...markets].map((m) => new PublicKey(m))
)
).map((programAccount) => {
return {
publicKey: programAccount?.publicKey,
account: new Market(
Market.getLayout(DEX_PID).decode(programAccount?.account.data),
-1, // Not used so don't bother fetching.
-1, // Not used so don't bother fetching.
provider.opts,
DEX_PID
),
};
});
const newMarketCache = new Map(marketCache);
marketAccounts.forEach((m) => {
newMarketCache.set(m.publicKey!.toString(), m.account);
});
setMarketCache(newMarketCache);
setOoAccounts(newOoAccounts);
});
}, [provider.connection, provider.wallet.publicKey, DEX_PID]);
return (
<SerumDexContext.Provider
value={{
openOrders: ooAccounts,
marketCache,
}}
>
{props.children}
</SerumDexContext.Provider>
);
}

View File

@ -25,13 +25,10 @@ import {
} from "@material-ui/core";
import { SettingsOutlined as Settings } from "@material-ui/icons";
import PopupState, { bindTrigger, bindPopover } from "material-ui-popup-state";
import {
useOpenOrders,
useMarket,
useMint,
useTokenList,
useSwapContext,
} from "./Context";
import { useSwapContext } from "./context/Swap";
import { useMarket, useOpenOrders } from "./context/Dex";
import { useTokenList } from "./context/TokenList";
import { useMint } from "./context/Mint";
const useStyles = makeStyles(() => ({
tab: {
@ -161,7 +158,7 @@ function OpenOrdersAccounts() {
<TableCell align="right">Base Free</TableCell>
<TableCell align="right">Quote Used</TableCell>
<TableCell align="right">Quote Free</TableCell>
<TableCell align="right">Open Orders</TableCell>
<TableCell align="right">Open Orders Account</TableCell>
<TableCell align="right">Action</TableCell>
</TableRow>
</TableHead>
@ -191,13 +188,13 @@ function OpenOrdersRow({
const [ooAccount, setOoAccount] = useState(openOrders[0]);
const marketClient = useMarket(market);
const tokenList = useTokenList();
const base = useMint(marketClient!.baseMintAddress);
const quote = useMint(marketClient!.quoteMintAddress);
const base = useMint(marketClient?.baseMintAddress);
const quote = useMint(marketClient?.quoteMintAddress);
const baseTicker = tokenList
.filter((t) => t.address === marketClient!.baseMintAddress.toString())
.filter((t) => t.address === marketClient?.baseMintAddress.toString())
.map((t) => t.symbol)[0];
const quoteTicker = tokenList
.filter((t) => t.address === marketClient!.quoteMintAddress.toString())
.filter((t) => t.address === marketClient?.quoteMintAddress.toString())
.map((t) => t.symbol)[0];
const marketName =
baseTicker && quoteTicker

View File

@ -12,16 +12,11 @@ import {
TextField,
} from "@material-ui/core";
import { Info, ExpandMore } from "@material-ui/icons";
import {
MintContextProvider,
SwapContextProvider,
TokenListContextProvider,
SerumDexContextProvider,
useSwapContext,
useTokenList,
useOwnedTokenAccount,
useMint,
} from "./Context";
import { SwapContextProvider, useSwapContext } from "./context/Swap";
import { DexContextProvider } from "./context/Dex";
import { MintContextProvider, useMint } from "./context/Mint";
import { TokenListContextProvider, useTokenList } from "./context/TokenList";
import { TokenContextProvider, useOwnedTokenAccount } from "./context/Token";
import TokenDialog from "./TokenDialog";
import { SettingsButton } from "./Settings";
@ -72,19 +67,21 @@ export default function Swap({
}) {
const swapClient = new SwapClient(provider, tokenList);
return (
<MintContextProvider provider={provider}>
<SwapContextProvider swapClient={swapClient}>
<TokenListContextProvider tokenList={tokenList}>
<SerumDexContextProvider provider={provider}>
<SwapInner style={style} />
</SerumDexContextProvider>
</TokenListContextProvider>
</SwapContextProvider>
</MintContextProvider>
<TokenListContextProvider tokenList={tokenList}>
<MintContextProvider provider={provider}>
<TokenContextProvider provider={provider}>
<DexContextProvider provider={provider}>
<SwapContextProvider swapClient={swapClient}>
<SwapCard style={style} />
</SwapContextProvider>
</DexContextProvider>
</TokenContextProvider>
</MintContextProvider>
</TokenListContextProvider>
);
}
function SwapInner({ style }: { style?: any }) {
function SwapCard({ style }: { style?: any }) {
const styles = useStyles();
return (
<div style={style}>
@ -295,7 +292,7 @@ function TokenName({ mint }: { mint: PublicKey }) {
function SwapButton() {
const styles = useStyles();
const { fromMint, toMint, fromAmount, minExpectedAmount } = useSwapContext();
const { fromMint, toMint, fromAmount, slippage } = useSwapContext();
const sendSwapTransaction = async () => {
console.log("sending swap");

View File

@ -10,8 +10,9 @@ import {
ListItem,
Typography,
} from "@material-ui/core";
import { useSwapContext, useTokenList, USDC_MINT, USDT_MINT } from "./Context";
import { TokenIcon } from "./Swap";
import { useSwapContext, USDC_MINT, USDT_MINT } from "./context/Swap";
import { useTokenList } from "./context/TokenList";
const useStyles = makeStyles((theme) => ({
dialogContent: {

View File

@ -0,0 +1,132 @@
import React, { useContext, useState, useEffect } from "react";
import { useAsync } from "react-async-hook";
import * as anchor from "@project-serum/anchor";
import { Provider } from "@project-serum/anchor";
import { Market, OpenOrders } from "@project-serum/serum";
import { PublicKey } from "@solana/web3.js";
const DEX_PID = new PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin");
const _DexContext = React.createContext<DexContext | null>(null);
type DexContext = {
// Maps market address to open orders accounts.
openOrders: Map<string, Array<OpenOrders>>;
marketCache: Map<string, Market>;
setMarketCache: (c: Map<string, Market>) => void;
provider: Provider;
};
export function DexContextProvider(props: any) {
const [ooAccounts, setOoAccounts] = useState<Map<string, Array<OpenOrders>>>(
new Map()
);
const [marketCache, setMarketCache] = useState<Map<string, Market>>(
new Map()
);
const provider = props.provider;
// Two operations:
// 1. Fetch all open orders accounts for the connected wallet.
// 2. Batch fetch all market accounts.
useEffect(() => {
OpenOrders.findForOwner(
provider.connection,
provider.wallet.publicKey,
DEX_PID
).then(async (openOrders) => {
const newOoAccounts = new Map();
let markets = new Set<string>();
openOrders.forEach((oo) => {
markets.add(oo.market.toString());
if (newOoAccounts.get(oo.market.toString())) {
newOoAccounts.get(oo.market.toString()).push(oo);
} else {
newOoAccounts.set(oo.market.toString(), [oo]);
}
});
if (markets.size > 100) {
// Punt request chunking until there's user demand.
throw new Error(
"Too many markets. Please file an issue to update this"
);
}
const marketAccounts = (
await anchor.utils.getMultipleAccounts(
provider.connection,
// @ts-ignore
[...markets].map((m) => new PublicKey(m))
)
).map((programAccount) => {
return {
publicKey: programAccount?.publicKey,
account: new Market(
Market.getLayout(DEX_PID).decode(programAccount?.account.data),
-1, // Not used so don't bother fetching.
-1, // Not used so don't bother fetching.
provider.opts,
DEX_PID
),
};
});
setMarketCache((marketCache) => {
const newMarketCache = new Map(marketCache);
marketAccounts.forEach((m) => {
newMarketCache.set(m.publicKey!.toString(), m.account);
});
return newMarketCache;
});
setOoAccounts(newOoAccounts);
});
}, [provider.connection, provider.wallet.publicKey, provider.opts]);
return (
<_DexContext.Provider
value={{
openOrders: ooAccounts,
marketCache,
setMarketCache,
provider,
}}
>
{props.children}
</_DexContext.Provider>
);
}
export function useOpenOrders(): Map<string, Array<OpenOrders>> {
const ctx = useContext(_DexContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx.openOrders;
}
export function useMarket(market: PublicKey): Market | undefined {
const ctx = useContext(_DexContext);
if (ctx === null) {
throw new Error("Context not available");
}
const asyncMarket = useAsync(async () => {
if (ctx.marketCache.get(market.toString())) {
return ctx.marketCache.get(market.toString());
}
const marketClient = await Market.load(
ctx.provider.connection,
market,
undefined,
DEX_PID
);
let cache = new Map(ctx.marketCache);
cache.set(market.toString(), marketClient);
ctx.setMarketCache(cache);
return marketClient;
}, [ctx.provider.connection, market]);
if (asyncMarket.result) {
return asyncMarket.result;
}
return undefined;
}

View File

@ -0,0 +1,64 @@
import React, { useContext, useState } from "react";
import { useAsync } from "react-async-hook";
import { Provider } from "@project-serum/anchor";
import { PublicKey, Account } from "@solana/web3.js";
import { MintInfo, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
const _MintContext = React.createContext<null | MintContext>(null);
type MintContext = {
mintCache: Map<string, MintInfo>;
setMintCache: (m: Map<string, MintInfo>) => void;
provider: Provider;
};
export function MintContextProvider(props: any) {
const provider = props.provider;
const [mintCache, setMintCache] = useState(new Map<string, MintInfo>());
return (
<_MintContext.Provider
value={{
mintCache,
setMintCache,
provider,
}}
>
{props.children}
</_MintContext.Provider>
);
}
export function useMint(mint?: PublicKey): MintInfo | undefined | null {
const ctx = useContext(_MintContext);
if (ctx === null) {
throw new Error("Mint context not found");
}
// Lazy load the mint account if needeed.
const asyncMintInfo = useAsync(async () => {
if (!mint) {
return undefined;
}
if (ctx.mintCache.get(mint.toString())) {
return ctx.mintCache.get(mint.toString());
}
const mintClient = new Token(
ctx.provider.connection,
mint,
TOKEN_PROGRAM_ID,
new Account()
);
const mintInfo = await mintClient.getMintInfo();
let cache = new Map(ctx.mintCache);
cache.set(mint.toString(), mintInfo);
ctx.setMintCache(cache);
return mintInfo;
}, [ctx.provider.connection, mint]);
if (asyncMintInfo.result) {
return asyncMintInfo.result;
}
return undefined;
}

View File

@ -0,0 +1,89 @@
import React, { useContext, useState } from "react";
import { Swap as SwapClient } from "@project-serum/swap";
import { PublicKey } from "@solana/web3.js";
import { MintInfo } from "@solana/spl-token";
const SRM_MINT = new PublicKey("SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt");
export const USDC_MINT = new PublicKey(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
);
export const USDT_MINT = new PublicKey(
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
);
const _SwapContext = React.createContext<null | SwapContext>(null);
export function SwapContextProvider(props: any) {
const swapClient = props.swapClient;
const [fromMint, setFromMint] = useState(SRM_MINT);
const [toMint, setToMint] = useState(USDC_MINT);
const [fromAmount, _setFromAmount] = useState(0);
const [toAmount, _setToAmount] = useState(0);
// Percent units.
const [slippage, setSlippage] = useState(0.5);
const swapToFromMints = () => {
const oldFrom = fromMint;
const oldFromAmount = fromAmount;
const oldTo = toMint;
const oldToAmount = toAmount;
setFromMint(oldTo);
setToMint(oldFrom);
_setFromAmount(oldToAmount);
_setToAmount(oldFromAmount);
};
const setFromAmount = (amount: number) => {
_setFromAmount(amount);
};
const setToAmount = (amount: number) => {
_setToAmount(amount);
};
return (
<_SwapContext.Provider
value={{
swapClient,
fromMint,
setFromMint,
toMint,
setToMint,
fromAmount,
setFromAmount,
toAmount,
setToAmount,
swapToFromMints,
slippage,
setSlippage,
}}
>
{props.children}
</_SwapContext.Provider>
);
}
export function useSwapContext(): SwapContext {
const ctx = useContext(_SwapContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx;
}
export type SwapContext = {
swapClient: SwapClient;
fromMint: PublicKey;
setFromMint: (m: PublicKey) => void;
toMint: PublicKey;
setToMint: (m: PublicKey) => void;
fromAmount: number;
setFromAmount: (a: number) => void;
toAmount: number;
setToAmount: (a: number) => void;
swapToFromMints: () => void;
fromMintAccount?: MintInfo;
toMintAccount?: MintInfo;
slippage: number;
setSlippage: (n: number) => void;
};

View File

@ -0,0 +1,65 @@
import React, { useContext, useState, useEffect } from "react";
import { PublicKey } from "@solana/web3.js";
import { AccountInfo as TokenAccount } from "@solana/spl-token";
import { getOwnedTokenAccounts } from "../../utils/tokens";
const _TokenContext = React.createContext<TokenContext | null>(null);
export function TokenContextProvider(props: any) {
const provider = props.provider;
const [ownedTokenAccounts, setOwnedTokenAccounts] = useState(undefined);
// Fetch all the owned token accounts for the wallet.
useEffect(() => {
getOwnedTokenAccounts(provider.connection, provider.wallet.publicKey).then(
setOwnedTokenAccounts
);
}, [provider.wallet.publicKey, provider.connection]);
return (
<_TokenContext.Provider
value={{
ownedTokenAccounts,
}}
>
{props.children}
</_TokenContext.Provider>
);
}
export type TokenContext = {
ownedTokenAccounts:
| { publicKey: PublicKey; account: TokenAccount }[]
| undefined;
};
// Null => none exists.
// Undefined => loading.
export function useOwnedTokenAccount(
mint: PublicKey
): { publicKey: PublicKey; account: TokenAccount } | null | undefined {
const ctx = useContext(_TokenContext);
if (ctx === null) {
throw new Error("Context not available");
}
if (ctx.ownedTokenAccounts === undefined) {
return undefined;
}
const tokenAccounts = ctx.ownedTokenAccounts.filter((account) =>
account.account.mint.equals(mint)
);
if (tokenAccounts.length === 0) {
return null;
}
// Take the account with the most tokens in it.
tokenAccounts.sort((a, b) =>
a.account.amount < b.account.amount
? -1
: a.account.amount > b.account.amount
? 1
: 0
);
return tokenAccounts[0];
}

View File

@ -0,0 +1,24 @@
import React, { useContext } from "react";
import { TokenListContainer, TokenInfo } from "@solana/spl-token-registry";
const _TokenListContext = React.createContext<null | TokenListContext>(null);
export function TokenListContextProvider(props: any) {
return (
<_TokenListContext.Provider value={{ tokenList: props.tokenList }}>
{props.children}
</_TokenListContext.Provider>
);
}
type TokenListContext = {
tokenList: TokenListContainer;
};
export function useTokenList(): TokenInfo[] {
const ctx = useContext(_TokenListContext);
if (ctx === null) {
throw new Error("Context not available");
}
return ctx.tokenList.getList();
}