Compare commits
31 Commits
Author | SHA1 | Date |
---|---|---|
armaniferrante | d41271f9c9 | |
armaniferrante | 1aae052784 | |
armaniferrante | 9cc0495d4a | |
Shardul Aeer | 3f213aa5ae | |
Shardul Aeer | 80b0fa4605 | |
Armani Ferrante | 4a92cad65a | |
armaniferrante | 8333da03ad | |
armaniferrante | 318801a876 | |
Armani Ferrante | 879c212da6 | |
armaniferrante | 60c53df8b4 | |
secretshardul | e67506f523 | |
armaniferrante | fd035017a5 | |
Armani Ferrante | 958f809be5 | |
Armani Ferrante | 671225aec8 | |
armaniferrante | c64caec9d5 | |
armaniferrante | 999f234165 | |
armaniferrante | 921c22e4e0 | |
Armani Ferrante | a3e841bf8d | |
Armani Ferrante | 15c9845bdc | |
secretshardul | 65284636de | |
Armani Ferrante | a589adcd3f | |
Armani Ferrante | 799b67aa92 | |
Armani Ferrante | 1ef329d34a | |
secretshardul | 12e7f94d8e | |
Armani Ferrante | 09d67dd99f | |
Armani Ferrante | 6125d18e3b | |
secretshardul | b23d21b0a7 | |
Armani Ferrante | 1886a484cb | |
secretshardul | edbc600b84 | |
Armani Ferrante | 5793336025 | |
Armani Ferrante | 5d745f3dd9 |
|
@ -1,6 +1,7 @@
|
|||
import "@fontsource/roboto";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { SnackbarProvider, useSnackbar } from "notistack";
|
||||
import { Button } from "@material-ui/core";
|
||||
import { Button, Grid, makeStyles } from "@material-ui/core";
|
||||
import { Provider } from "@project-serum/anchor";
|
||||
// @ts-ignore
|
||||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
|
@ -10,6 +11,7 @@ import {
|
|||
Connection,
|
||||
Transaction,
|
||||
TransactionSignature,
|
||||
PublicKey,
|
||||
} from "@solana/web3.js";
|
||||
import {
|
||||
TokenListContainer,
|
||||
|
@ -30,7 +32,16 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
minHeight: "100vh",
|
||||
paddingLeft: theme.spacing(1),
|
||||
paddingRight: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
function AppInner() {
|
||||
const styles = useStyles();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [tokenList, setTokenList] = useState<TokenListContainer | null>(null);
|
||||
|
@ -90,20 +101,11 @@ function AppInner() {
|
|||
}, [wallet, enqueueSnackbar]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "450px",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
<Grid
|
||||
container
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
className={styles.root}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
@ -113,10 +115,17 @@ function AppInner() {
|
|||
{!isConnected ? "Connect" : "Disconnect"}
|
||||
</Button>
|
||||
{tokenList && <Swap provider={provider} tokenList={tokenList} />}
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
// Cast wallet to AnchorWallet in order to be compatible with Anchor's Provider class
|
||||
interface AnchorWallet {
|
||||
signTransaction(tx: Transaction): Promise<Transaction>;
|
||||
signAllTransactions(txs: Transaction[]): Promise<Transaction[]>;
|
||||
publicKey: PublicKey;
|
||||
}
|
||||
|
||||
// Custom provider to display notifications whenever a transaction is sent.
|
||||
//
|
||||
// Note that this is an Anchor wallet/network provider--not a React provider,
|
||||
|
@ -136,7 +145,8 @@ class NotifyingProvider extends Provider {
|
|||
opts: ConfirmOptions,
|
||||
onTransaction: (tx: TransactionSignature | undefined, err?: Error) => void
|
||||
) {
|
||||
super(connection, wallet, opts);
|
||||
const newWallet = wallet as AnchorWallet;
|
||||
super(connection, newWallet, opts);
|
||||
this.onTransaction = onTransaction;
|
||||
}
|
||||
|
||||
|
@ -150,7 +160,9 @@ class NotifyingProvider extends Provider {
|
|||
this.onTransaction(txSig);
|
||||
return txSig;
|
||||
} catch (err) {
|
||||
this.onTransaction(undefined, err);
|
||||
if (err instanceof Error || err === undefined) {
|
||||
this.onTransaction(undefined, err);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +178,9 @@ class NotifyingProvider extends Provider {
|
|||
});
|
||||
return txSigs;
|
||||
} catch (err) {
|
||||
this.onTransaction(undefined, err);
|
||||
if (err instanceof Error || err === undefined) {
|
||||
this.onTransaction(undefined, err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;700&display=swap");
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
|
|
71
package.json
71
package.json
|
@ -1,24 +1,23 @@
|
|||
{
|
||||
"name": "@project-serum/swap-ui",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"homepage": "https://github.com/project-serum/swap-ui",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^4.3.0",
|
||||
"@project-serum/serum": "^0.13.34",
|
||||
"@project-serum/swap": "^0.1.0-alpha.20",
|
||||
"@solana/spl-token": "^0.1.4"
|
||||
"@project-serum/serum": "^0.13.58",
|
||||
"@project-serum/swap": "^0.1.0-alpha.35",
|
||||
"@solana/spl-token": "^0.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||
"@project-serum/anchor": "^0.7.0",
|
||||
"@solana/spl-token-registry": "^0.2.86",
|
||||
"@solana/web3.js": "^1.10.1",
|
||||
"material-ui-popup-state": "^1.8.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@project-serum/anchor": "^0.14.0",
|
||||
"@solana/spl-token-registry": "^0.2.229",
|
||||
"@solana/web3.js": "^1.17.1",
|
||||
"material-ui-popup-state": "^1.9.3",
|
||||
"react": "^17.0.2",
|
||||
"react-async-hook": "^3.6.2"
|
||||
},
|
||||
|
@ -26,7 +25,8 @@
|
|||
"lint": "prettier src/** example/src/** --check",
|
||||
"lint:fix": "prettier src/** example/src/** -w",
|
||||
"build": "rm -rf dist && tsc --build tsconfig.json",
|
||||
"docs": "typedoc --excludePrivate --out ./docs src/index.tsx --includeVersion --readme none"
|
||||
"docs": "typedoc --excludePrivate --out ./docs src/index.tsx --includeVersion --readme none",
|
||||
"prepare": "tsc --build tsconfig.json"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
@ -35,38 +35,39 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@fontsource/roboto": "4.5.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||
"@project-serum/anchor": "^0.7.0",
|
||||
"@project-serum/serum": "^0.13.34",
|
||||
"@project-serum/sol-wallet-adapter": "^0.2.0",
|
||||
"@project-serum/swap": "^0.1.0-alpha.20",
|
||||
"@solana/spl-token": "^0.1.4",
|
||||
"@solana/spl-token-registry": "^0.2.86",
|
||||
"@solana/web3.js": "^1.10.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@project-serum/anchor": "^0.14.0",
|
||||
"@project-serum/serum": "^0.13.58",
|
||||
"@project-serum/sol-wallet-adapter": "^0.2.5",
|
||||
"@project-serum/swap": "^0.1.0-alpha.35",
|
||||
"@solana/spl-token": "^0.1.8",
|
||||
"@solana/spl-token-registry": "^0.2.229",
|
||||
"@solana/web3.js": "^1.24.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.10",
|
||||
"@types/react": "^17.0.19",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"bs58": "^4.0.1",
|
||||
"gh-pages": "^3.1.0",
|
||||
"material-ui-popup-state": "^1.8.3",
|
||||
"notistack": "^1.0.7",
|
||||
"prettier": "^2.3.0",
|
||||
"gh-pages": "^3.2.3",
|
||||
"material-ui-popup-state": "^1.9.3",
|
||||
"notistack": "^1.0.10",
|
||||
"prettier": "^2.3.2",
|
||||
"react": "^17.0.2",
|
||||
"react-app-rewire-alias": "^1.0.3",
|
||||
"react-app-rewired": "^2.1.8",
|
||||
"react-async-hook": "^3.6.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"typedoc": "^0.20.36",
|
||||
"typescript": "^4.1.2",
|
||||
"web-vitals": "^1.0.1"
|
||||
"typedoc": "^0.21.9",
|
||||
"typescript": "^4.4.2",
|
||||
"web-vitals": "^2.1.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import { useState } from "react";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { BN } from "@project-serum/anchor";
|
||||
import {
|
||||
PublicKey,
|
||||
Keypair,
|
||||
Transaction,
|
||||
SystemProgram,
|
||||
Signer,
|
||||
} from "@solana/web3.js";
|
||||
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import { BN, Provider } from "@project-serum/anchor";
|
||||
import {
|
||||
makeStyles,
|
||||
Card,
|
||||
|
@ -16,7 +23,7 @@ import {
|
|||
useOpenOrders,
|
||||
useRouteVerbose,
|
||||
useMarket,
|
||||
BASE_TAKER_FEE_BPS,
|
||||
FEE_MULTIPLIER,
|
||||
} from "../context/Dex";
|
||||
import { useTokenMap } from "../context/TokenList";
|
||||
import { useMint, useOwnedTokenAccount } from "../context/Token";
|
||||
|
@ -24,13 +31,14 @@ import { useCanSwap, useReferral } from "../context/Swap";
|
|||
import TokenDialog from "./TokenDialog";
|
||||
import { SettingsButton } from "./Settings";
|
||||
import { InfoLabel } from "./Info";
|
||||
import { SOL_MINT, WRAPPED_SOL_MINT } from "../utils/pubkeys";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
card: {
|
||||
width: "450px",
|
||||
borderRadius: "16px",
|
||||
width: theme.spacing(50),
|
||||
borderRadius: theme.spacing(2),
|
||||
boxShadow: "0px 0px 30px 5px rgba(0,0,0,0.075)",
|
||||
padding: "16px",
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
tab: {
|
||||
width: "50%",
|
||||
|
@ -40,12 +48,12 @@ const useStyles = makeStyles((theme) => ({
|
|||
},
|
||||
swapButton: {
|
||||
width: "100%",
|
||||
borderRadius: "10px",
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
padding: "10px",
|
||||
padding: theme.spacing(1.5),
|
||||
},
|
||||
swapToFromButton: {
|
||||
display: "block",
|
||||
|
@ -60,14 +68,14 @@ const useStyles = makeStyles((theme) => ({
|
|||
textAlign: "right",
|
||||
},
|
||||
swapTokenFormContainer: {
|
||||
borderRadius: "10px",
|
||||
borderRadius: theme.spacing(2),
|
||||
boxShadow: "0px 0px 15px 2px rgba(33,150,243,0.1)",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: "10px",
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
swapTokenSelectorContainer: {
|
||||
marginLeft: "5px",
|
||||
marginLeft: theme.spacing(1),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "50%",
|
||||
|
@ -78,7 +86,7 @@ const useStyles = makeStyles((theme) => ({
|
|||
fontSize: "14px",
|
||||
},
|
||||
maxButton: {
|
||||
marginLeft: 10,
|
||||
marginLeft: theme.spacing(1),
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 700,
|
||||
fontSize: "12px",
|
||||
|
@ -88,7 +96,7 @@ const useStyles = makeStyles((theme) => ({
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
marginBottom: "10px",
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -116,13 +124,13 @@ export default function SwapCard({
|
|||
);
|
||||
}
|
||||
|
||||
function SwapHeader() {
|
||||
export function SwapHeader() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "20px",
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
|
@ -180,7 +188,7 @@ function SwapToForm({ style }: { style?: any }) {
|
|||
);
|
||||
}
|
||||
|
||||
function SwapTokenForm({
|
||||
export function SwapTokenForm({
|
||||
from,
|
||||
style,
|
||||
mint,
|
||||
|
@ -261,10 +269,11 @@ function TokenButton({
|
|||
onClick: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div onClick={onClick} className={styles.tokenButton}>
|
||||
<TokenIcon mint={mint} style={{ width: "30px" }} />
|
||||
<TokenIcon mint={mint} style={{ width: theme.spacing(4) }} />
|
||||
<TokenName mint={mint} style={{ fontSize: 14, fontWeight: 700 }} />
|
||||
<ExpandMore />
|
||||
</div>
|
||||
|
@ -293,15 +302,23 @@ export function TokenIcon({ mint, style }: { mint: PublicKey; style: any }) {
|
|||
|
||||
function TokenName({ mint, style }: { mint: PublicKey; style: any }) {
|
||||
const tokenMap = useTokenMap();
|
||||
const theme = useTheme();
|
||||
let tokenInfo = tokenMap.get(mint.toString());
|
||||
|
||||
return (
|
||||
<Typography style={{ marginLeft: "10px", marginRight: "5px", ...style }}>
|
||||
<Typography
|
||||
style={{
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(1),
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{tokenInfo?.symbol}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
function SwapButton() {
|
||||
export function SwapButton() {
|
||||
const styles = useStyles();
|
||||
const {
|
||||
fromMint,
|
||||
|
@ -325,9 +342,11 @@ function SwapButton() {
|
|||
const canSwap = useCanSwap();
|
||||
const referral = useReferral(fromMarket);
|
||||
const fair = useSwapFair();
|
||||
const fromWallet = useOwnedTokenAccount(fromMint);
|
||||
const toWallet = useOwnedTokenAccount(toMint);
|
||||
const quoteMint = useMint(fromMarket && fromMarket.quoteMintAddress);
|
||||
let fromWallet = useOwnedTokenAccount(fromMint);
|
||||
let toWallet = useOwnedTokenAccount(toMint);
|
||||
const quoteMint = fromMarket && fromMarket.quoteMintAddress;
|
||||
const quoteMintInfo = useMint(quoteMint);
|
||||
const quoteWallet = useOwnedTokenAccount(quoteMint);
|
||||
|
||||
// Click handler.
|
||||
const sendSwapTransaction = async () => {
|
||||
|
@ -337,43 +356,90 @@ function SwapButton() {
|
|||
if (!fair) {
|
||||
throw new Error("Invalid fair");
|
||||
}
|
||||
if (!quoteMint) {
|
||||
if (!quoteMint || !quoteMintInfo) {
|
||||
throw new Error("Quote mint not found");
|
||||
}
|
||||
|
||||
const amount = new BN(fromAmount * 10 ** fromMintInfo.decimals);
|
||||
const minExchangeRate = {
|
||||
rate: new BN(
|
||||
(10 ** toMintInfo.decimals * (1 - BASE_TAKER_FEE_BPS)) / fair
|
||||
)
|
||||
.muln(100 - slippage)
|
||||
.divn(100),
|
||||
fromDecimals: fromMintInfo.decimals,
|
||||
quoteDecimals: quoteMint.decimals,
|
||||
strict: isStrict,
|
||||
};
|
||||
const fromOpenOrders = fromMarket
|
||||
? openOrders.get(fromMarket?.address.toString())
|
||||
: undefined;
|
||||
const toOpenOrders = toMarket
|
||||
? openOrders.get(toMarket?.address.toString())
|
||||
: undefined;
|
||||
await swapClient.swap({
|
||||
fromMint,
|
||||
toMint,
|
||||
fromWallet: fromWallet ? fromWallet.publicKey : undefined,
|
||||
toWallet: toWallet ? toWallet.publicKey : undefined,
|
||||
amount,
|
||||
minExchangeRate,
|
||||
referral,
|
||||
// Pass in the below parameters so that the client doesn't perform
|
||||
// wasteful network requests when we already have the data.
|
||||
fromMarket,
|
||||
toMarket,
|
||||
fromOpenOrders: fromOpenOrders ? fromOpenOrders[0].address : undefined,
|
||||
toOpenOrders: toOpenOrders ? toOpenOrders[0].address : undefined,
|
||||
// Auto close newly created open orders accounts.
|
||||
close: isClosingNewAccounts,
|
||||
});
|
||||
const isSol = fromMint.equals(SOL_MINT) || toMint.equals(SOL_MINT);
|
||||
const wrappedSolAccount = isSol ? Keypair.generate() : undefined;
|
||||
|
||||
// Build the swap.
|
||||
let txs = await (async () => {
|
||||
if (!fromMarket) {
|
||||
throw new Error("Market undefined");
|
||||
}
|
||||
|
||||
const minExchangeRate = {
|
||||
rate: new BN((10 ** toMintInfo.decimals * FEE_MULTIPLIER) / fair)
|
||||
.muln(100 - slippage)
|
||||
.divn(100),
|
||||
fromDecimals: fromMintInfo.decimals,
|
||||
quoteDecimals: quoteMintInfo.decimals,
|
||||
strict: isStrict,
|
||||
};
|
||||
const fromOpenOrders = fromMarket
|
||||
? openOrders.get(fromMarket?.address.toString())
|
||||
: undefined;
|
||||
const toOpenOrders = toMarket
|
||||
? openOrders.get(toMarket?.address.toString())
|
||||
: undefined;
|
||||
const fromWalletAddr = fromMint.equals(SOL_MINT)
|
||||
? wrappedSolAccount!.publicKey
|
||||
: fromWallet
|
||||
? fromWallet.publicKey
|
||||
: undefined;
|
||||
const toWalletAddr = toMint.equals(SOL_MINT)
|
||||
? wrappedSolAccount!.publicKey
|
||||
: toWallet
|
||||
? toWallet.publicKey
|
||||
: undefined;
|
||||
|
||||
return await swapClient.swapTxs({
|
||||
fromMint,
|
||||
toMint,
|
||||
quoteMint,
|
||||
amount,
|
||||
minExchangeRate,
|
||||
referral,
|
||||
fromMarket,
|
||||
toMarket,
|
||||
// Automatically created if undefined.
|
||||
fromOpenOrders: fromOpenOrders ? fromOpenOrders[0].address : undefined,
|
||||
toOpenOrders: toOpenOrders ? toOpenOrders[0].address : undefined,
|
||||
fromWallet: fromWalletAddr,
|
||||
toWallet: toWalletAddr,
|
||||
quoteWallet: quoteWallet ? quoteWallet.publicKey : undefined,
|
||||
// Auto close newly created open orders accounts.
|
||||
close: isClosingNewAccounts,
|
||||
});
|
||||
})();
|
||||
|
||||
// If swapping SOL, then insert a wrap/unwrap instruction.
|
||||
if (isSol) {
|
||||
if (txs.length > 1) {
|
||||
throw new Error("SOL must be swapped in a single transaction");
|
||||
}
|
||||
const { tx: wrapTx, signers: wrapSigners } = await wrapSol(
|
||||
swapClient.program.provider,
|
||||
wrappedSolAccount as Keypair,
|
||||
fromMint,
|
||||
amount
|
||||
);
|
||||
const { tx: unwrapTx, signers: unwrapSigners } = unwrapSol(
|
||||
swapClient.program.provider,
|
||||
wrappedSolAccount as Keypair
|
||||
);
|
||||
const tx = new Transaction();
|
||||
tx.add(wrapTx);
|
||||
tx.add(txs[0].tx);
|
||||
tx.add(unwrapTx);
|
||||
txs[0].tx = tx;
|
||||
txs[0].signers.push(...wrapSigners);
|
||||
txs[0].signers.push(...unwrapSigners);
|
||||
}
|
||||
|
||||
await swapClient.program.provider.sendAll(txs);
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
|
@ -386,3 +452,63 @@ function SwapButton() {
|
|||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
async function wrapSol(
|
||||
provider: Provider,
|
||||
wrappedSolAccount: Keypair,
|
||||
fromMint: PublicKey,
|
||||
amount: BN
|
||||
): Promise<{ tx: Transaction; signers: Array<Signer | undefined> }> {
|
||||
const tx = new Transaction();
|
||||
const signers = [wrappedSolAccount];
|
||||
// Create new, rent exempt account.
|
||||
tx.add(
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: provider.wallet.publicKey,
|
||||
newAccountPubkey: wrappedSolAccount.publicKey,
|
||||
lamports: await Token.getMinBalanceRentForExemptAccount(
|
||||
provider.connection
|
||||
),
|
||||
space: 165,
|
||||
programId: TOKEN_PROGRAM_ID,
|
||||
})
|
||||
);
|
||||
// Transfer lamports. These will be converted to an SPL balance by the
|
||||
// token program.
|
||||
if (fromMint.equals(SOL_MINT)) {
|
||||
tx.add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: provider.wallet.publicKey,
|
||||
toPubkey: wrappedSolAccount.publicKey,
|
||||
lamports: amount.toNumber(),
|
||||
})
|
||||
);
|
||||
}
|
||||
// Initialize the account.
|
||||
tx.add(
|
||||
Token.createInitAccountInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
WRAPPED_SOL_MINT,
|
||||
wrappedSolAccount.publicKey,
|
||||
provider.wallet.publicKey
|
||||
)
|
||||
);
|
||||
return { tx, signers };
|
||||
}
|
||||
|
||||
function unwrapSol(
|
||||
provider: Provider,
|
||||
wrappedSolAccount: Keypair
|
||||
): { tx: Transaction; signers: Array<Signer | undefined> } {
|
||||
const tx = new Transaction();
|
||||
tx.add(
|
||||
Token.createCloseAccountInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
wrappedSolAccount.publicKey,
|
||||
provider.wallet.publicKey,
|
||||
provider.wallet.publicKey,
|
||||
[]
|
||||
)
|
||||
);
|
||||
return { tx, signers: [] };
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from "@material-ui/core";
|
||||
import { TokenIcon } from "./Swap";
|
||||
import { useSwappableTokens } from "../context/TokenList";
|
||||
import { useMediaQuery } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
dialogContent: {
|
||||
|
@ -53,6 +54,7 @@ export default function TokenDialog({
|
|||
const styles = useStyles();
|
||||
const { swappableTokens, swappableTokensSollet, swappableTokensWormhole } =
|
||||
useSwappableTokens();
|
||||
const displayTabs = !useMediaQuery("(max-width:450px)");
|
||||
const selectedTokens =
|
||||
tabSelection === 0
|
||||
? swappableTokens
|
||||
|
@ -107,34 +109,36 @@ export default function TokenDialog({
|
|||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Tabs
|
||||
value={tabSelection}
|
||||
onChange={(e, v) => setTabSelection(v)}
|
||||
classes={{
|
||||
indicator: styles.tabIndicator,
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
value={0}
|
||||
className={styles.tab}
|
||||
classes={{ selected: styles.tabSelected }}
|
||||
label="Main"
|
||||
/>
|
||||
<Tab
|
||||
value={1}
|
||||
className={styles.tab}
|
||||
classes={{ selected: styles.tabSelected }}
|
||||
label="Wormhole"
|
||||
/>
|
||||
<Tab
|
||||
value={2}
|
||||
className={styles.tab}
|
||||
classes={{ selected: styles.tabSelected }}
|
||||
label="Sollet"
|
||||
/>
|
||||
</Tabs>
|
||||
</DialogActions>
|
||||
{displayTabs && (
|
||||
<DialogActions>
|
||||
<Tabs
|
||||
value={tabSelection}
|
||||
onChange={(e, v) => setTabSelection(v)}
|
||||
classes={{
|
||||
indicator: styles.tabIndicator,
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
value={0}
|
||||
className={styles.tab}
|
||||
classes={{ selected: styles.tabSelected }}
|
||||
label="Main"
|
||||
/>
|
||||
<Tab
|
||||
value={1}
|
||||
className={styles.tab}
|
||||
classes={{ selected: styles.tabSelected }}
|
||||
label="Wormhole"
|
||||
/>
|
||||
<Tab
|
||||
value={2}
|
||||
className={styles.tab}
|
||||
classes={{ selected: styles.tabSelected }}
|
||||
label="Sollet"
|
||||
/>
|
||||
</Tabs>
|
||||
</DialogActions>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useContext, useState, useEffect } from "react";
|
||||
import * as assert from "assert";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { TokenInfo } from "@solana/spl-token-registry";
|
||||
import { MintLayout } from "@solana/spl-token";
|
||||
|
@ -14,6 +15,8 @@ import {
|
|||
DEX_PID,
|
||||
USDC_MINT,
|
||||
USDT_MINT,
|
||||
SOL_MINT,
|
||||
WRAPPED_SOL_MINT,
|
||||
WORM_USDC_MINT,
|
||||
WORM_USDT_MINT,
|
||||
WORM_USDC_MARKET,
|
||||
|
@ -24,7 +27,8 @@ import { useTokenMap, useTokenListContext } from "./TokenList";
|
|||
import { fetchSolletInfo, requestWormholeSwapMarketIfNeeded } from "./Sollet";
|
||||
import { setMintCache } from "./Token";
|
||||
|
||||
export const BASE_TAKER_FEE_BPS = 0.0022;
|
||||
const BASE_TAKER_FEE_BPS = 0.0022;
|
||||
export const FEE_MULTIPLIER = 1 - BASE_TAKER_FEE_BPS;
|
||||
|
||||
type DexContext = {
|
||||
// Maps market address to open orders accounts.
|
||||
|
@ -94,19 +98,14 @@ export function DexContextProvider(props: any) {
|
|||
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.
|
||||
-1, // Set below so that we can batch fetch mints.
|
||||
-1, // Set below so that we can batch fetch mints.
|
||||
swapClient.program.provider.opts,
|
||||
DEX_PID
|
||||
),
|
||||
};
|
||||
});
|
||||
marketClients.forEach((m) => {
|
||||
_MARKET_CACHE.set(
|
||||
m.publicKey!.toString(),
|
||||
new Promise<Market>((resolve) => resolve(m.account))
|
||||
);
|
||||
});
|
||||
|
||||
setOoAccounts(newOoAccounts);
|
||||
|
||||
// Batch fetch all the mints, since we know we'll need them at some
|
||||
|
@ -131,8 +130,28 @@ export function DexContextProvider(props: any) {
|
|||
swapClient.program.provider.connection,
|
||||
mintPubkeys
|
||||
);
|
||||
mints.forEach((mint) => {
|
||||
setMintCache(mint!.publicKey, MintLayout.decode(mint!.account.data));
|
||||
const mintInfos = mints.map((mint) => {
|
||||
const mintInfo = MintLayout.decode(mint!.account.data);
|
||||
setMintCache(mint!.publicKey, mintInfo);
|
||||
return { publicKey: mint!.publicKey, mintInfo };
|
||||
});
|
||||
|
||||
marketClients.forEach((m) => {
|
||||
const baseMintInfo = mintInfos.filter((mint) =>
|
||||
mint.publicKey.equals(m.account.baseMintAddress)
|
||||
)[0];
|
||||
const quoteMintInfo = mintInfos.filter((mint) =>
|
||||
mint.publicKey.equals(m.account.quoteMintAddress)
|
||||
)[0];
|
||||
assert.ok(baseMintInfo && quoteMintInfo);
|
||||
// @ts-ignore
|
||||
m.account._baseSplTokenDecimals = baseMintInfo.mintInfo.decimals;
|
||||
// @ts-ignore
|
||||
m.account._quoteSplTokenDecimals = quoteMintInfo.mintInfo.decimals;
|
||||
_MARKET_CACHE.set(
|
||||
m.publicKey!.toString(),
|
||||
new Promise<Market>((resolve) => resolve(m.account))
|
||||
);
|
||||
});
|
||||
});
|
||||
}, [
|
||||
|
@ -179,15 +198,11 @@ export function useMarket(market?: PublicKey): Market | undefined {
|
|||
}
|
||||
|
||||
const marketClient = new Promise<Market>(async (resolve) => {
|
||||
const marketAccount =
|
||||
await swapClient.program.provider.connection.getAccountInfo(market);
|
||||
if (marketAccount === null) {
|
||||
throw new Error("Invalid market");
|
||||
}
|
||||
const marketClient = new Market(
|
||||
Market.getLayout(DEX_PID).decode(marketAccount.data),
|
||||
-1,
|
||||
-1,
|
||||
// TODO: if we already have the mints, then pass them through to the
|
||||
// market client here to save a network request.
|
||||
const marketClient = await Market.load(
|
||||
swapClient.program.provider.connection,
|
||||
market,
|
||||
swapClient.program.provider.opts,
|
||||
DEX_PID
|
||||
);
|
||||
|
@ -343,6 +358,9 @@ export function useBbo(market?: PublicKey): Bbo | undefined {
|
|||
}
|
||||
const bestBid = orderbook.bids.items(true).next().value;
|
||||
const bestOffer = orderbook.asks.items(false).next().value;
|
||||
if (!bestBid && !bestOffer) {
|
||||
return {};
|
||||
}
|
||||
if (!bestBid) {
|
||||
return { bestOffer: bestOffer.price };
|
||||
}
|
||||
|
@ -373,7 +391,11 @@ export function useFairRoute(
|
|||
if (fromMarket === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (fromMarket?.baseMintAddress.equals(fromMint)) {
|
||||
if (
|
||||
fromMarket?.baseMintAddress.equals(fromMint) ||
|
||||
(fromMarket?.baseMintAddress.equals(WRAPPED_SOL_MINT) &&
|
||||
fromMint.equals(SOL_MINT))
|
||||
) {
|
||||
return fromBbo.bestBid && 1.0 / fromBbo.bestBid;
|
||||
} else {
|
||||
return fromBbo.bestOffer && fromBbo.bestOffer;
|
||||
|
@ -426,7 +448,10 @@ export function useRouteVerbose(
|
|||
const [wormholeMarket, kind] = swapMarket;
|
||||
return { markets: [wormholeMarket], kind };
|
||||
}
|
||||
const markets = swapClient.route(fromMint, toMint);
|
||||
const markets = swapClient.route(
|
||||
fromMint.equals(SOL_MINT) ? WRAPPED_SOL_MINT : fromMint,
|
||||
toMint.equals(SOL_MINT) ? WRAPPED_SOL_MINT : toMint
|
||||
);
|
||||
if (markets === null) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as assert from "assert";
|
||||
import React, { useContext, useState } from "react";
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import {
|
||||
|
@ -9,12 +9,18 @@ import {
|
|||
} from "@solana/spl-token";
|
||||
import { Market } from "@project-serum/serum";
|
||||
import { SRM_MINT, USDC_MINT, USDT_MINT } from "../utils/pubkeys";
|
||||
import { useFairRoute, useRouteVerbose, useDexContext } from "./Dex";
|
||||
import {
|
||||
useFairRoute,
|
||||
useRouteVerbose,
|
||||
useDexContext,
|
||||
FEE_MULTIPLIER,
|
||||
} from "./Dex";
|
||||
import {
|
||||
useTokenListContext,
|
||||
SPL_REGISTRY_SOLLET_TAG,
|
||||
SPL_REGISTRY_WORM_TAG,
|
||||
} from "./TokenList";
|
||||
import { useOwnedTokenAccount } from "../context/Token";
|
||||
|
||||
const DEFAULT_SLIPPAGE_PERCENT = 0.5;
|
||||
|
||||
|
@ -83,31 +89,40 @@ export function SwapContextProvider(props: any) {
|
|||
|
||||
assert.ok(slippage >= 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fair) {
|
||||
return;
|
||||
}
|
||||
setFromAmount(fromAmount);
|
||||
}, [fair]);
|
||||
|
||||
const swapToFromMints = () => {
|
||||
const oldFrom = fromMint;
|
||||
const oldFromAmount = fromAmount;
|
||||
const oldTo = toMint;
|
||||
const oldToAmount = toAmount;
|
||||
_setFromAmount(oldToAmount);
|
||||
setFromMint(oldTo);
|
||||
setToMint(oldFrom);
|
||||
_setFromAmount(oldToAmount);
|
||||
_setToAmount(oldFromAmount);
|
||||
};
|
||||
|
||||
const setFromAmount = (amount: number) => {
|
||||
if (fair === undefined) {
|
||||
throw new Error("Fair price not found");
|
||||
_setFromAmount(0);
|
||||
_setToAmount(0);
|
||||
return;
|
||||
}
|
||||
_setFromAmount(amount);
|
||||
_setToAmount(amount / fair);
|
||||
_setToAmount(FEE_MULTIPLIER * (amount / fair));
|
||||
};
|
||||
|
||||
const setToAmount = (amount: number) => {
|
||||
if (fair === undefined) {
|
||||
throw new Error("Fair price not found");
|
||||
_setFromAmount(0);
|
||||
_setToAmount(0);
|
||||
return;
|
||||
}
|
||||
_setToAmount(amount);
|
||||
_setFromAmount(amount * fair);
|
||||
_setFromAmount((amount * fair) / FEE_MULTIPLIER);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -166,12 +181,20 @@ export function useCanSwap(): boolean {
|
|||
const { fromMint, toMint, fromAmount, toAmount } = useSwapContext();
|
||||
const { swapClient } = useDexContext();
|
||||
const { wormholeMap, solletMap } = useTokenListContext();
|
||||
const fromWallet = useOwnedTokenAccount(fromMint);
|
||||
const fair = useSwapFair();
|
||||
const route = useRouteVerbose(fromMint, toMint);
|
||||
if (route === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
// From wallet exists.
|
||||
fromWallet !== undefined &&
|
||||
fromWallet !== null &&
|
||||
// Fair price is defined.
|
||||
fair !== undefined &&
|
||||
fair > 0 &&
|
||||
// Mints are distinct.
|
||||
fromMint.equals(toMint) === false &&
|
||||
// Wallet is connected.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useContext, useState, useEffect } from "react";
|
||||
import * as assert from "assert";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { Provider } from "@project-serum/anchor";
|
||||
import { Provider, BN } from "@project-serum/anchor";
|
||||
import { PublicKey, Account } from "@solana/web3.js";
|
||||
import {
|
||||
MintInfo,
|
||||
|
@ -9,7 +9,11 @@ import {
|
|||
Token,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { getOwnedTokenAccounts, parseTokenAccountData } from "../utils/tokens";
|
||||
import {
|
||||
getOwnedAssociatedTokenAccounts,
|
||||
parseTokenAccountData,
|
||||
} from "../utils/tokens";
|
||||
import { SOL_MINT } from "../utils/pubkeys";
|
||||
|
||||
export type TokenContext = {
|
||||
provider: Provider;
|
||||
|
@ -27,14 +31,33 @@ export function TokenContextProvider(props: any) {
|
|||
setRefresh((r) => r + 1);
|
||||
return;
|
||||
}
|
||||
getOwnedTokenAccounts(provider.connection, provider.wallet.publicKey).then(
|
||||
(accs) => {
|
||||
if (accs) {
|
||||
_OWNED_TOKEN_ACCOUNTS_CACHE.push(...accs);
|
||||
// Fetch SPL tokens.
|
||||
getOwnedAssociatedTokenAccounts(
|
||||
provider.connection,
|
||||
provider.wallet.publicKey
|
||||
).then((accs) => {
|
||||
if (accs) {
|
||||
// @ts-ignore
|
||||
_OWNED_TOKEN_ACCOUNTS_CACHE.push(...accs);
|
||||
setRefresh((r) => r + 1);
|
||||
}
|
||||
});
|
||||
// Fetch SOL balance.
|
||||
provider.connection
|
||||
.getAccountInfo(provider.wallet.publicKey)
|
||||
.then((acc: { lamports: number }) => {
|
||||
if (acc) {
|
||||
_OWNED_TOKEN_ACCOUNTS_CACHE.push({
|
||||
publicKey: provider.wallet.publicKey,
|
||||
// @ts-ignore
|
||||
account: {
|
||||
amount: new BN(acc.lamports),
|
||||
mint: SOL_MINT,
|
||||
},
|
||||
});
|
||||
setRefresh((r) => r + 1);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}, [provider.wallet.publicKey, provider.connection]);
|
||||
|
||||
return (
|
||||
|
@ -69,23 +92,28 @@ export function useOwnedTokenAccount(
|
|||
|
||||
// Take the account with the most tokens in it.
|
||||
tokenAccounts.sort((a, b) =>
|
||||
a.account.amount < b.account.amount
|
||||
a.account.amount > b.account.amount
|
||||
? -1
|
||||
: a.account.amount > b.account.amount
|
||||
: a.account.amount < b.account.amount
|
||||
? 1
|
||||
: 0
|
||||
);
|
||||
|
||||
const tokenAccount = tokenAccounts[0];
|
||||
let tokenAccount = tokenAccounts[0];
|
||||
const isSol = mint?.equals(SOL_MINT);
|
||||
|
||||
// Stream updates when the balance changes.
|
||||
useEffect(() => {
|
||||
let listener: number;
|
||||
if (tokenAccount) {
|
||||
// SOL is special cased since it's not an SPL token.
|
||||
if (tokenAccount && isSol) {
|
||||
listener = provider.connection.onAccountChange(
|
||||
tokenAccount.publicKey,
|
||||
(info) => {
|
||||
const token = parseTokenAccountData(info.data);
|
||||
provider.wallet.publicKey,
|
||||
(info: { lamports: number }) => {
|
||||
const token = {
|
||||
amount: new BN(info.lamports),
|
||||
mint: SOL_MINT,
|
||||
} as TokenAccount;
|
||||
if (token.amount !== tokenAccount.account.amount) {
|
||||
const index = _OWNED_TOKEN_ACCOUNTS_CACHE.indexOf(tokenAccount);
|
||||
assert.ok(index >= 0);
|
||||
|
@ -95,6 +123,27 @@ export function useOwnedTokenAccount(
|
|||
}
|
||||
);
|
||||
}
|
||||
// SPL tokens.
|
||||
else if (tokenAccount) {
|
||||
listener = provider.connection.onAccountChange(
|
||||
tokenAccount.publicKey,
|
||||
(info) => {
|
||||
if (info.data.length !== 0) {
|
||||
try {
|
||||
const token = parseTokenAccountData(info.data);
|
||||
if (token.amount !== tokenAccount.account.amount) {
|
||||
const index = _OWNED_TOKEN_ACCOUNTS_CACHE.indexOf(tokenAccount);
|
||||
assert.ok(index >= 0);
|
||||
_OWNED_TOKEN_ACCOUNTS_CACHE[index].account = token;
|
||||
setRefresh((r) => r + 1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Failed to decode token AccountInfo");
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
if (listener) {
|
||||
provider.connection.removeAccountChangeListener(listener);
|
||||
|
@ -106,7 +155,7 @@ export function useOwnedTokenAccount(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (tokenAccounts.length === 0) {
|
||||
if (!isSol && tokenAccounts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -152,4 +201,7 @@ const _OWNED_TOKEN_ACCOUNTS_CACHE: Array<{
|
|||
}> = [];
|
||||
|
||||
// Cache storing all previously fetched mint infos.
|
||||
const _MINT_CACHE = new Map<string, Promise<MintInfo>>();
|
||||
// @ts-ignore
|
||||
const _MINT_CACHE = new Map<string, Promise<MintInfo>>([
|
||||
[SOL_MINT.toString(), { decimals: 9 }],
|
||||
]);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useContext, useMemo } from "react";
|
||||
import { TokenInfo } from "@solana/spl-token-registry";
|
||||
import { USDC_MINT, USDT_MINT } from "../utils/pubkeys";
|
||||
import { SOL_MINT } from "../utils/pubkeys";
|
||||
|
||||
type TokenListContext = {
|
||||
tokenMap: Map<string, TokenInfo>;
|
||||
|
@ -18,11 +18,32 @@ export const SPL_REGISTRY_SOLLET_TAG = "wrapped-sollet";
|
|||
// Tag in the spl-token-registry for wormhole wrapped tokens.
|
||||
export const SPL_REGISTRY_WORM_TAG = "wormhole";
|
||||
|
||||
const SOL_TOKEN_INFO = {
|
||||
chainId: 101,
|
||||
address: SOL_MINT.toString(),
|
||||
name: "Native SOL",
|
||||
decimals: "9",
|
||||
symbol: "SOL",
|
||||
logoURI:
|
||||
"https://cdn.jsdelivr.net/gh/trustwallet/assets@master/blockchains/solana/info/logo.png",
|
||||
tags: [],
|
||||
extensions: {
|
||||
website: "https://solana.com/",
|
||||
serumV3Usdc: "9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT",
|
||||
serumV3Usdt: "HWHvQhFmJB3NUcu1aihKmrKegfVxBEHzwVX6yZCKEsi1",
|
||||
coingeckoId: "solana",
|
||||
waterfallbot: "https://t.me/SOLwaterfall",
|
||||
},
|
||||
};
|
||||
|
||||
export function TokenListContextProvider(props: any) {
|
||||
const tokenList = useMemo(
|
||||
() => props.tokenList.filterByClusterSlug("mainnet-beta").getList(),
|
||||
[props.tokenList]
|
||||
);
|
||||
const tokenList = useMemo(() => {
|
||||
const list = props.tokenList.filterByClusterSlug("mainnet-beta").getList();
|
||||
// Manually add a fake SOL mint for the native token. The component is
|
||||
// opinionated in that it distinguishes between wrapped SOL and SOL.
|
||||
list.push(SOL_TOKEN_INFO);
|
||||
return list;
|
||||
}, [props.tokenList]);
|
||||
|
||||
// Token map for quick lookup.
|
||||
const tokenMap = useMemo(() => {
|
||||
|
@ -35,14 +56,11 @@ export function TokenListContextProvider(props: any) {
|
|||
|
||||
// Tokens with USD(x) quoted markets.
|
||||
const swappableTokens = useMemo(() => {
|
||||
const tokens = tokenList
|
||||
.filter((t: TokenInfo) => {
|
||||
const isUsdxQuoted =
|
||||
t.extensions?.serumV3Usdt || t.extensions?.serumV3Usdc;
|
||||
const isSol =
|
||||
t.address === "So11111111111111111111111111111111111111112";
|
||||
return isUsdxQuoted && !isSol;
|
||||
})
|
||||
const tokens = tokenList.filter((t: TokenInfo) => {
|
||||
const isUsdxQuoted =
|
||||
t.extensions?.serumV3Usdt || t.extensions?.serumV3Usdc;
|
||||
return isUsdxQuoted;
|
||||
});
|
||||
tokens.sort((a: TokenInfo, b: TokenInfo) =>
|
||||
a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0
|
||||
);
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;700&display=swap");
|
|
@ -1,19 +1,33 @@
|
|||
import "@fontsource/roboto";
|
||||
import { ReactElement } from "react";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { TokenListContainer } from "@solana/spl-token-registry";
|
||||
import { Provider } from "@project-serum/anchor";
|
||||
import { Swap as SwapClient } from "@project-serum/swap";
|
||||
import { SwapContextProvider } from "./context/Swap";
|
||||
import { DexContextProvider } from "./context/Dex";
|
||||
import { TokenListContextProvider } from "./context/TokenList";
|
||||
import { TokenContextProvider } from "./context/Token";
|
||||
import SwapCard from "./components/Swap";
|
||||
import {
|
||||
createMuiTheme,
|
||||
ThemeOptions,
|
||||
ThemeProvider,
|
||||
} from "@material-ui/core/styles";
|
||||
import {
|
||||
SwapContextProvider,
|
||||
useSwapContext,
|
||||
useSwapFair,
|
||||
} from "./context/Swap";
|
||||
import {
|
||||
DexContextProvider,
|
||||
useBbo,
|
||||
useFairRoute,
|
||||
useMarketName,
|
||||
} from "./context/Dex";
|
||||
import { TokenListContextProvider, useTokenMap } from "./context/TokenList";
|
||||
import { TokenContextProvider, useMint } from "./context/Token";
|
||||
import SwapCard, {
|
||||
ArrowButton,
|
||||
SwapButton,
|
||||
SwapHeader,
|
||||
SwapTokenForm,
|
||||
} from "./components/Swap";
|
||||
import TokenDialog from "./components/TokenDialog";
|
||||
|
||||
/**
|
||||
* A`Swap` component that can be embedded into applications. To use,
|
||||
|
@ -30,7 +44,7 @@ import {
|
|||
* For information on other properties like earning referrals, see the
|
||||
* [[SwapProps]] documentation.
|
||||
*/
|
||||
export function Swap(props: SwapProps): ReactElement {
|
||||
export default function Swap(props: SwapProps): ReactElement {
|
||||
const {
|
||||
containerStyle,
|
||||
contentStyle,
|
||||
|
@ -44,6 +58,8 @@ export function Swap(props: SwapProps): ReactElement {
|
|||
toAmount,
|
||||
referral,
|
||||
} = props;
|
||||
|
||||
// @ts-ignore
|
||||
const swapClient = new SwapClient(provider, tokenList);
|
||||
const theme = createMuiTheme(
|
||||
materialTheme || {
|
||||
|
@ -153,4 +169,29 @@ export type SwapProps = {
|
|||
swapTokenContainerStyle?: any;
|
||||
};
|
||||
|
||||
export default Swap;
|
||||
export {
|
||||
// Components.
|
||||
Swap,
|
||||
SwapCard,
|
||||
SwapHeader,
|
||||
SwapTokenForm,
|
||||
ArrowButton,
|
||||
SwapButton,
|
||||
TokenDialog,
|
||||
// Providers and context.
|
||||
// Swap.
|
||||
SwapContextProvider,
|
||||
useSwapContext,
|
||||
useSwapFair,
|
||||
// TokenList.
|
||||
TokenListContextProvider,
|
||||
useTokenMap,
|
||||
// Token.
|
||||
TokenContextProvider,
|
||||
useMint,
|
||||
// Dex.
|
||||
DexContextProvider,
|
||||
useFairRoute,
|
||||
useMarketName,
|
||||
useBbo,
|
||||
};
|
||||
|
|
|
@ -16,6 +16,15 @@ export const USDT_MINT = new PublicKey(
|
|||
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
||||
);
|
||||
|
||||
// Arbitrary mint to represent SOL (not wrapped SOL).
|
||||
export const SOL_MINT = new PublicKey(
|
||||
"Ejmc1UB4EsES5oAaRN63SpoxMJidt3ZGBrqrZk49vjTZ"
|
||||
);
|
||||
|
||||
export const WRAPPED_SOL_MINT = new PublicKey(
|
||||
"So11111111111111111111111111111111111111112"
|
||||
);
|
||||
|
||||
export const WORM_MARKET_BASE = new PublicKey(
|
||||
"6a9wpsZpZGxGhFVSQBpcTNjNjytdbSA1iUw1A5KNDxPw"
|
||||
);
|
||||
|
|
|
@ -5,62 +5,58 @@ import * as BufferLayout from "buffer-layout";
|
|||
import { BN } from "@project-serum/anchor";
|
||||
import {
|
||||
TOKEN_PROGRAM_ID,
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
AccountInfo as TokenAccount,
|
||||
} from "@solana/spl-token";
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import * as bs58 from "bs58";
|
||||
|
||||
export async function getOwnedTokenAccounts(
|
||||
export async function getOwnedAssociatedTokenAccounts(
|
||||
connection: Connection,
|
||||
publicKey: PublicKey
|
||||
) {
|
||||
let filters = getOwnedAccountsFilters(publicKey);
|
||||
// @ts-ignore
|
||||
let resp = await connection._rpcRequest("getProgramAccounts", [
|
||||
TOKEN_PROGRAM_ID.toBase58(),
|
||||
{
|
||||
commitment: connection.commitment,
|
||||
filters,
|
||||
},
|
||||
]);
|
||||
if (resp.error) {
|
||||
throw new Error(
|
||||
"failed to get token accounts owned by " +
|
||||
publicKey.toBase58() +
|
||||
": " +
|
||||
resp.error.message
|
||||
);
|
||||
}
|
||||
return resp.result
|
||||
let resp = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, {
|
||||
commitment: connection.commitment,
|
||||
filters,
|
||||
});
|
||||
|
||||
const accs = resp
|
||||
.map(({ pubkey, account: { data, executable, owner, lamports } }: any) => ({
|
||||
publicKey: new PublicKey(pubkey),
|
||||
accountInfo: {
|
||||
data: bs58.decode(data),
|
||||
data,
|
||||
executable,
|
||||
owner: new PublicKey(owner),
|
||||
lamports,
|
||||
},
|
||||
}))
|
||||
.filter(({ accountInfo }: any) => {
|
||||
// TODO: remove this check once mainnet is updated
|
||||
return filters.every((filter) => {
|
||||
if (filter.dataSize) {
|
||||
return accountInfo.data.length === filter.dataSize;
|
||||
} else if (filter.memcmp) {
|
||||
let filterBytes = bs58.decode(filter.memcmp.bytes);
|
||||
return accountInfo.data
|
||||
.slice(
|
||||
filter.memcmp.offset,
|
||||
filter.memcmp.offset + filterBytes.length
|
||||
)
|
||||
.equals(filterBytes);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
})
|
||||
.map(({ publicKey, accountInfo }: any) => {
|
||||
return { publicKey, account: parseTokenAccountData(accountInfo.data) };
|
||||
});
|
||||
|
||||
return (
|
||||
(
|
||||
await Promise.all(
|
||||
accs
|
||||
// @ts-ignore
|
||||
.map(async (ta) => {
|
||||
const ata = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
ta.account.mint,
|
||||
publicKey
|
||||
);
|
||||
return [ta, ata];
|
||||
})
|
||||
)
|
||||
)
|
||||
// @ts-ignore
|
||||
.filter(([ta, ata]) => ta.publicKey.equals(ata))
|
||||
// @ts-ignore
|
||||
.map(([ta]) => ta)
|
||||
);
|
||||
}
|
||||
|
||||
const ACCOUNT_LAYOUT = BufferLayout.struct([
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
|
|
Loading…
Reference in New Issue